Online Compiler logoOnline Compiler

JavaScript async/await: A Complete Guide

async/await is a modern JavaScript feature that makes working with promises much easier. It allows you to write asynchronous code that looks and behaves like synchronous code, making it more readable and maintainable.

What is async/await?

async/await is syntactic sugar built on top of promises. It allows you to write asynchronous code that looks synchronous, making it easier to read and understand. The async keyword declares an asynchronous function, and awaitpauses execution until a promise resolves.

Basic async/await Example

// Traditional Promise approach
function fetchUser() {
  return fetch('/api/user')
    .then(response => response.json())
    .then(data => {
      console.log('User:', data);
      return data;
    });
}

...

async/await makes asynchronous code look synchronous and easier to follow.

async Functions

An async function is a function declared with the async keyword. It always returns a promise, even if you return a primitive value.

async Function Declaration and Expression

// async function declaration
async function getData() {
  return 'Hello, World!';
}

// async function expression
const getData2 = async function() {
  return 'Hello from expression!';
};

...

async functions always return promises and can be declared in multiple ways.

The await Keyword

The await keyword can only be used inside async functions. It pauses the execution of the async function until the promise resolves, then returns the resolved value.

Using await with Promises

async function fetchMultipleResources() {
  console.log('Starting fetch...');
  
  // These run sequentially (not in parallel)
  const userResponse = await fetch('/api/user');
  const user = await userResponse.json();
  console.log('User loaded:', user.name);
  
  const postsResponse = await fetch('/api/posts');
  const posts = await postsResponse.json();
...

await pauses execution until promises resolve. Use Promise.all for parallel operations.

Error Handling with try/catch

In async functions, use traditional try/catch blocks for error handling. This is much cleaner than chaining .catch() methods.

Error Handling in async/await

// ❌ Without proper error handling
async function riskyOperation() {
  const response = await fetch('/api/data');
  const data = await response.json();
  return data;
}

// ✅ With proper error handling
async function safeOperation() {
  try {
...

Use try/catch blocks in async functions for clean error handling.

Converting Promises to async/await

Promise Chain to async/await

// Original promise chain
function getUserData() {
  return fetch('/api/user')
    .then(response => {
      if (!response.ok) {
        throw new Error('Failed to fetch user');
      }
      return response.json();
    })
    .then(user => {
...

Convert complex promise chains to cleaner async/await syntax.

Sequential vs Parallel Execution

Sequential vs Parallel Operations

// Sequential execution (slower)
async function sequentialOperations() {
  console.time('Sequential');
  
  const user1 = await fetch('/api/users/1').then(r => r.json());
  const user2 = await fetch('/api/users/2').then(r => r.json());
  const user3 = await fetch('/api/users/3').then(r => r.json());
  
  console.timeEnd('Sequential');
  return [user1, user2, user3];
...

Use Promise.all() with await for parallel operations to improve performance.

Top-level await

In modern JavaScript (ES2022+), you can use await at the top level of modules, outside of async functions. This is useful for module initialization.

Top-level await in Modules

// config.js
export const config = await fetch('/api/config').then(r => r.json());

// main.js
import { config } from './config.js';

// config is already resolved when imported
console.log('App config:', config);

// In browsers, use type="module"
...

Top-level await allows using await outside async functions in module contexts.

Async Iterators and Generators

Async Generators

// Async generator function
async function* asyncGenerator() {
  yield await Promise.resolve('First');
  yield await Promise.resolve('Second');
  yield await Promise.resolve('Third');
}

// Using async generator
async function processAsyncGenerator() {
  for await (const value of asyncGenerator()) {
...

Async generators allow yielding promises and using for-await-of loops.

Common Patterns and Best Practices

Retry Pattern with async/await

async function retryAsync(fn, maxRetries = 3, delay = 1000) {
  for (let i = 0; i <= maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      if (i === maxRetries) {
        throw error;
      }
      console.log(`Attempt ${i + 1} failed, retrying...`);
      await new Promise(resolve => setTimeout(resolve, delay));
...

Implement retry logic for unreliable async operations using async/await.

Timeout Pattern

function withTimeout(promise, timeoutMs) {
  return Promise.race([
    promise,
    new Promise((_, reject) => 
      setTimeout(() => reject(new Error('Timeout')), timeoutMs)
    )
  ]);
}

async function fetchWithTimeout(url, timeoutMs = 5000) {
...

Add timeout functionality to async operations to prevent hanging requests.

Real-World Example: API Service

Complete API Service with async/await

class ApiService {
  constructor(baseURL) {
    this.baseURL = baseURL;
  }

  async request(endpoint, options = {}) {
    const url = `${this.baseURL}${endpoint}`;
    
    const response = await fetch(url, {
      headers: {
...

Build a complete API service using async/await for all operations.

async/await vs Promises: When to Use What

Use async/await when:

  • You need sequential async operations
  • You want cleaner, more readable code
  • You're working with complex promise chains
  • You need try/catch error handling

Use Promises when:

  • You need Promise.all(), Promise.race(), etc.
  • You're working with callback-based APIs
  • You need to return promises from non-async functions
  • You want functional programming approaches

Mixing async/await and Promises

// Perfectly valid to mix both approaches
async function complexOperation() {
  // Use async/await for sequential operations
  const user = await fetch('/api/user').then(r => r.json());
  
  // Use Promise.all for parallel operations
  const [posts, comments] = await Promise.all([
    fetch(`/api/posts?userId=${user.id}`).then(r => r.json()),
    fetch(`/api/comments?userId=${user.id}`).then(r => r.json())
  ]);
...

Mix async/await and promises for optimal async programming.

Key Takeaways

  • async functions always return promises and enable await
  • await pauses execution until promises resolve
  • Use try/catch for error handling in async functions
  • Combine Promise.all() with await for parallel operations
  • async/await makes async code look synchronous and easier to read
  • Mix async/await with promises when appropriate
  • Always handle errors to prevent unhandled promise rejections

Want to learn the fundamentals? Check out our Promises guide to understand the foundation of async JavaScript.