What is async/await?

👨‍💻 Frontend Developer 🟠 May come up 🎚️ Medium
#JavaScript #Asynchronicity #JS Basics

Brief Answer

Async/await is syntax sugar over promises in JavaScript that allows writing asynchronous code as if it were synchronous. The async keyword before a function makes it asynchronous and guarantees to return a promise, while await suspends function execution until the promise resolves. This simplifies reading and writing asynchronous code, avoiding .then() chains.

Key benefits:

  • Readability — linear code structure instead of .then() chains
  • Simplified error handling — using try/catch instead of .catch()
  • Debugging convenience — call stack preservation

Full Answer

Async/await is a modern way to work with asynchronous code in JavaScript, introduced in ES2017. It allows writing asynchronous code that looks and behaves like synchronous code, making it more readable and maintainable.

How async/await Works

The async and await keywords are always used together:

// Declaring an asynchronous function
async function fetchData() {
  // await suspends execution until promise resolves
  const response = await fetch('/api/data');
  const data = await response.json();
  return data;
}
 
// Usage
fetchData()
  .then(result => console.log('Data:', result))
  .catch(error => console.error('Error:', error));

Main Use Cases

1. Working with HTTP Requests

Async/await is especially convenient when working with APIs:

// With promises
function getUserDataWithPromises(userId) {
  return fetch(`/api/users/${userId}`)
    .then(response => response.json())
    .then(user => {
      return fetch(`/api/users/${userId}/posts`)
        .then(response => response.json())
        .then(posts => ({ user, posts }));
    })
    .catch(error => console.error('Error:', error));
}
 
// With async/await
async function getUserDataWithAsync(userId) {
  try {
    const userResponse = await fetch(`/api/users/${userId}`);
    const user = await userResponse.json();
    
    const postsResponse = await fetch(`/api/users/${userId}/posts`);
    const posts = await postsResponse.json();
    
    return { user, posts };
  } catch (error) {
    console.error('Error:', error);
    throw error;
  }
}

2. Sequential Execution of Operations

When operations need to be executed one after another:

async function processUserData() {
  try {
    // Step 1: Get user data
    const user = await fetchUser();
    console.log('User received:', user.name);
    
    // Step 2: Get user profile
    const profile = await fetchProfile(user.id);
    console.log('Profile received:', profile.email);
    
    // Step 3: Get user preferences
    const preferences = await fetchPreferences(profile.id);
    console.log('Preferences received:', preferences.theme);
    
    return { user, profile, preferences };
  } catch (error) {
    console.error('Error processing user data:', error);
    throw error;
  }
}

3. Parallel Execution of Operations

Sometimes multiple independent operations need to be executed in parallel:

// Sequential execution (slower)
async function sequentialExecution() {
  const users = await fetchUsers();        // 1 second
  const posts = await fetchPosts();        // 1 second
  const comments = await fetchComments();  // 1 second
  // Total time: ~3 seconds
  return { users, posts, comments };
}
 
// Parallel execution (faster)
async function parallelExecution() {
  const [users, posts, comments] = await Promise.all([
    fetchUsers(),      // 1 second
    fetchPosts(),      // 1 second
    fetchComments()    // 1 second
  ]);
  // Total time: ~1 second
  return { users, posts, comments };
}

Practical Examples

Form Handling with Validation

async function handleSubmit(formData) {
  try {
    // Validate data
    const validation = await validateForm(formData);
    if (!validation.isValid) {
      throw new Error('Form contains errors');
    }
    
    // Submit data
    const response = await fetch('/api/submit', {
      method: 'POST',
      body: JSON.stringify(formData),
      headers: { 'Content-Type': 'application/json' }
    });
    
    if (!response.ok) {
      throw new Error('Error submitting data');
    }
    
    const result = await response.json();
    console.log('Form submitted successfully:', result);
    return result;
  } catch (error) {
    console.error('Error submitting form:', error);
    showErrorToUser(error.message);
  }
}

Working with localStorage and API

async function loadUserData(userId) {
  try {
    // First check cache
    const cachedData = localStorage.getItem(`user_${userId}`);
    if (cachedData) {
      console.log('Data loaded from cache');
      return JSON.parse(cachedData);
    }
    
    // If not in cache, load from API
    const response = await fetch(`/api/users/${userId}`);
    if (!response.ok) {
      throw new Error('Error loading user data');
    }
    
    const userData = await response.json();
    
    // Save to cache
    localStorage.setItem(`user_${userId}`, JSON.stringify(userData));
    
    return userData;
  } catch (error) {
    console.error('Error loading user data:', error);
    throw error;
  }
}

Handling Multiple Data Sources

async function fetchUserDataFromMultipleSources(userId) {
  try {
    // Load data from main API
    const mainDataPromise = fetch(`/api/users/${userId}`).then(res => res.json());
    
    // Load data from backup API
    const backupDataPromise = fetch(`/backup-api/users/${userId}`).then(res => res.json());
    
    // Wait for first successful result
    const userData = await Promise.any([mainDataPromise, backupDataPromise]);
    
    return userData;
  } catch (error) {
    if (error instanceof AggregateError) {
      console.error('All data sources unavailable:', error.errors);
    } else {
      console.error('Error loading data:', error);
    }
    throw error;
  }
}

Common Mistakes and Solutions

1. Forgotten await

// ❌ Forgotten await
async function badExample() {
  const data = fetch('/api/data'); // This is a promise, not data!
  console.log(data.name); // undefined
}
 
// ✅ Proper await usage
async function goodExample() {
  const response = await fetch('/api/data');
  const data = await response.json();
  console.log(data.name); // real data
}

2. Improper Error Handling

// ❌ Improper error handling
async function badErrorHandling() {
  const data = await fetch('/api/data').json(); // Error won't be caught properly
  return data;
}
 
// ✅ Proper error handling
async function goodErrorHandling() {
  try {
    const response = await fetch('/api/data');
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Error loading data:', error);
    throw error;
  }
}

3. Excessive await Usage

// ❌ Excessive await usage
async function badChaining() {
  const response = await fetch('/api/data');
  const data = await response.json();
  const processed = await processData(data);
  const validated = await validateData(processed);
  return validated;
}
 
// ✅ Chaining without excessive await
async function goodChaining() {
  const response = await fetch('/api/data');
  const data = await response.json();
  return validateData(processData(data));
}

Best Practices

  1. Always use try/catch — for error handling in async functions
  2. Use Promise.all for parallel operations — for performance improvements
  3. Don’t forget await — especially before methods returning promises
  4. Handle HTTP errors — check response.ok before calling .json()
  5. Avoid excessive await — don’t use await where it’s not needed

Comparison with Promises

FeaturePromisesAsync/Await
Readability.then() chainsLinear code
Error Handling.catch()try/catch
DebuggingHarder due to chainsEasier, call stack preserved
PerformanceSameSame
CompatibilityAll modern browsersRequires transpilation for older browsers

Key async/await Benefits

  1. Readability — code looks like synchronous
  2. Simplified debugging — call stack preserved
  3. Convenient error handling — familiar try/catch usage
  4. Ease of learning — easier for developers used to synchronous code
  5. Compatibility — works with any promises

Async/await is a powerful tool that makes asynchronous code more readable and maintainable. Understanding async/await is critically important for modern JavaScript development.


Knowledge Check Task

Task

What will be output to the console and why? Fix the code if necessary:

async function processData() {
  console.log('1. Function start');
  
  const data1 = await fetch('/api/data1');
  console.log('2. After first request');
  
  const data2 = fetch('/api/data2');
  console.log('3. After second request');
  
  const result1 = data1.json();
  console.log('4. After parsing first response');
  
  const result2 = await data2.json();
  console.log('5. After parsing second response');
  
  return { result1, result2 };
}
 
processData()
  .then(results => console.log('6. Results:', results))
  .catch(error => console.error('7. Error:', error.message));
View answer

Answer: The code contains several errors. Correct version:

async function processData() {
  console.log('1. Function start');
  
  const response1 = await fetch('/api/data1');
  console.log('2. After first request');
  
  const response2 = await fetch('/api/data2'); // Don't wait, just start
  console.log('3. After second request');
  
  const result1 = await response1.json(); // Added await
  console.log('4. After parsing first response');
  
  const result2 = await response2.json(); // await is already here
  console.log('5. After parsing second response');
  
  return { result1, result2 };
}
 
processData()
  .then(results => console.log('6. Results:', results))
  .catch(error => console.error('7. Error:', error.message));

Error explanations:

  1. Missing await for response1.json() — without await the .json() method returns a promise, not data
  2. Inconsistent await usage — for response2 await is used correctly, but for response1 it’s missing

Execution order:

  1. Will output “1. Function start”
  2. Execute fetch(‘/api/data1’) and suspend until completion
  3. Output “2. After first request”
  4. Start fetch(‘/api/data2’) without waiting
  5. Output “3. After second request”
  6. Execute response1.json() and suspend until completion
  7. Output “4. After parsing first response”
  8. Wait for response2.json() completion
  9. Output “5. After parsing second response”
  10. Return results
  11. Output “6. Results: [object Object]”

It’s important to understand that await suspends function execution until the promise resolves, but doesn’t block the entire execution thread.


Want more articles to prepare for interviews? Subscribe to EasyAdvice, bookmark the site and improve yourself every day 💪