Stackvora.tech

Async/Await Cheat Sheet

Asynchronous operations are fundamental to modern web development, allowing non-blocking execution crucial for responsive UIs and efficient server-side…

Asynchronous operations are fundamental to modern web development, allowing non-blocking execution crucial for responsive UIs and efficient server-side processing. JavaScript's async/await syntax, built atop Promises, provides a synchronous-looking approach to managing asynchronous code, significantly improving readability and maintainability compared to traditional callback patterns or raw Promise chains. This guide covers the core concepts, practical usage patterns, and error handling strategies for async/await in JavaScript and TypeScript.

Introduced in ECMAScript 2017 (ES8), async/await is a syntactic sugar over Promises. An async function always returns a Promise. The await keyword can only be used inside an async function and pauses the execution of that function until the awaited Promise settles (either fulfills or rejects).

Basic Syntax and Execution Flow

An async function is declared with the async keyword before the function keyword (or arrow function parameters). Inside an async function, you can use await to wait for a Promise to resolve.

// Async function declaration
async function fetchData(url: string): Promise<any> {
  console.log(`Fetching data from ${url}...`);
  const response = await fetch(url); // Pauses here until fetch() Promise resolves
  if (!response.ok) {
    throw new Error(`HTTP error! Status: ${response.status}`);
  }
  const data = await response.json(); // Pauses here until json() Promise resolves
  console.log(`Data from ${url} received.`);
  return data;
}

// Invoking an async function
async function main() {
  try {
    const userData = await fetchData('https://api.example.com/users/1');
    console.log('User data:', userData);
  } catch (error) {
    console.error('Failed to fetch user data:', error);
  }
}

main();
console.log("Main function invoked (this runs immediately, not waiting for fetchData to complete)");

When fetchData is called, it immediately returns a Promise. The main function then awaits this Promise. Inside fetchData, the first await fetch(url) pauses the fetchData function's execution until the network request completes. Once it resolves, the next line executes. This continues until the function returns a value or throws an error, at which point the Promise returned by fetchData is settled.

Error Handling with try...catch

async/await integrates seamlessly with standard try...catch blocks, providing a familiar mechanism for handling errors from asynchronous operations. If any awaited Promise rejects, the error is thrown and can be caught by the nearest catch block.

async function loadUser(id: string): Promise<any> {
  try {
    console.log(`Attempting to load user ${id}...`);
    const res = await fetch(`https://api.example.com/users/${id}`);

    // Check for non-2xx HTTP status codes
    if (!res.ok) {
      console.error(`HTTP error for user ${id}: ${res.status} ${res.statusText}`);
      // Throwing an error here ensures it's caught by the outer try/catch
      throw new Error(`HTTP ${res.status} for user ${id}`);
    }

    const userData = await res.json();
    console.log(`User ${id} loaded successfully.`);
    return userData;
  } catch (err: unknown) { // Use 'unknown' for safer type checking in TypeScript
    if (err instanceof Error) {
      console.error(`loadUser failed for ID ${id}: ${err.message}`);
    } else {
      console.error(`loadUser failed for ID ${id}: An unknown error occurred.`);
    }
    // Re-throw the error if callers need to handle it further
    throw err;
  }
}

async function processUsers() {
  console.log("Starting user processing...");
  try {
    const user1 = await loadUser("123"); // Assuming this succeeds
    console.log("Processed user 123:", user1);

    const userFail = await loadUser("nonexistent"); // Assuming this fails (e.g., 404)
    console.log("Processed user nonexistent:", userFail); // This line won't be reached
  } catch (outerError) {
    console.error("An error occurred during user processing:", outerError);
  }
  console.log("User processing finished.");
}

processUsers();

In this example, if fetch fails (e.g., network error) or returns a non-ok status, the catch block within loadUser will execute. If loadUser re-throws the error, the catch block in processUsers will then handle it.

Parallel Execution with Promise.all and Promise.allSettled

Using await sequentially for independent operations will block execution unnecessarily. For concurrent operations, Promise.all and Promise.allSettled are essential.

Promise.all(iterable)

Waits for all Promises in the iterable to fulfill. If any Promise rejects, Promise.all immediately rejects with the reason of the first Promise that rejected. The result is an array of fulfilled values, in the same order as the input Promises.

async function getMultipleUsers(ids: string[]): Promise<any[]> {
  try {
    console.log(`Loading users in parallel: ${ids.join(', ')}`);
    const userPromises = ids.map(id => loadUser(id));
    const users = await Promise.all(userPromises); // Waits for all to succeed
    console.log('All users loaded successfully.');
    return users;
  } catch (error) {
    console.error('One or more users failed to load:', error);
    throw error; // Re-throw to indicate overall failure
  }
}

// Example usage
// Assuming loadUser("456") might fail (e.g., 404)
getMultipleUsers(["123", "456", "789"])
  .then(users => console.log("Successfully retrieved all users:", users))
  .catch(err => console.error("Failed to retrieve some users, but got:", err));

Common use case for Promise.all:

async function initializeApplication() {
  try {
    const [config, userProfile, permissions] = await Promise.all([
      fetch('/api/config').then(res => res.json()),
      fetch('/api/profile').then(res => res.json()),
      fetch('/api/permissions').then(res => res.json())
    ]);
    console.log('Application initialized with:', { config, userProfile, permissions });
    // Proceed with app setup
  } catch (error) {
    console.error('Failed to initialize application components:', error);
    // Render an error page or show a toast notification
  }
}

Promise.allSettled(iterable)

Waits for all Promises in the iterable to settle (either fulfill or reject). It never rejects. The result is an array of objects, each describing the outcome of a Promise ({ status: 'fulfilled', value: ... } or { status: 'rejected', reason: ... }), maintaining the input order.

Useful when you want to proceed even if some operations fail, and you need to know the outcome of each individual operation.

async function getPartialUsers(ids: string[]): Promise<PromiseSettledResult<any>[]> {
  console.log(`Attempting to load partial users: ${ids.join(', ')}`);
  const userPromises = ids.map(id => loadUser(id).catch(err => ({ status: 'rejected', reason: err }))); // loadUser already handles its own error, but for Promise.allSettled it's often cleaner to let it handle raw promises
  
  // A more direct approach with Promise.allSettled:
  const results = await Promise.allSettled(ids.map(id => loadUser(id)));
  
  results.forEach((result, index) => {
    if (result.status === 'fulfilled') {
      console.log(`User ${ids[index]} loaded:`, result.value);
    } else {
      console.warn(`User ${ids[index]} failed:`, result.reason);
    }
  });
  return results;
}

getPartialUsers(["userA", "userB", "userC"]) // Assuming userB fails
  .then(results => console.log("All partial user load attempts completed:", results))
  .catch(err => console.error("Should not happen with Promise.allSettled, but caught:", err));

TypeScript Considerations

TypeScript enhances async/await with strong typing for function return values and error handling.

  • async functions always return Promise<T>, where T is the type of the value the function would synchronously return.
  • When catching errors, the type of the caught variable in a catch block is unknown by default in TypeScript 4.0+. This is a safer default as anything can be thrown. You should narrow the type (e.g., using instanceof Error) or assert it.
async function typedFetchData(url: string): Promise<{ id: number; name: string }> {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`Failed to fetch ${url}: ${response.status}`);
  }
  const data: { id: number; name: string } = await response.json();
  return data;
}

async function useTypedData() {
  try {
    const user = await typedFetchData('https://api.example.com/users/profile');
    // 'user' is correctly typed as { id: number; name: string }
    console.log(`User ID: ${user.id}, Name: ${user.name}`);
    return user;
  } catch (error: unknown) {
    if (error instanceof Error) {
      console.error('Error fetching typed data:', error.message);
    } else {
      console.error('An unexpected error occurred:', error);
    }
    throw error;
  }
}

Promise.race and Promise.any

Promise.race(iterable)

Returns a Promise that settles as soon as one of the Promises in the iterable settles. Its resolution or rejection matches the first Promise that settles. Useful for timeouts or fetching data from multiple sources, taking the fastest one.

async function fetchWithTimeout(url: string, timeoutMs: number): Promise<any> {
  const fetchPromise = fetch(url).then(res => {
    if (!res.ok) throw new Error(`HTTP Error: ${res.status}`);
    return res.json();
  });

  const timeoutPromise = new Promise<never>((_, reject) =>
    setTimeout(() => reject(new Error(`Timeout after ${timeoutMs} ms for ${url}`)), timeoutMs)
  );

  return Promise.race([fetchPromise, timeoutPromise]);
}

async function getDataOrTimeout() {
  try {
    const data = await fetchWithTimeout('https://api.example.com/slow-service', 1000); // 1-second timeout
    console.log('Received data within timeout:', data);
  } catch (error) {
    console.error('Failed to get data or timed out:', error);
  }
}

getDataOrTimeout();

Promise.any(iterable) (ES2021+)

Returns a Promise that fulfills as soon as one of the Promises in the iterable fulfills. If all Promises reject, it rejects with an AggregateError, containing all rejection reasons. Useful when you need at least one successful result from multiple independent operations.

async function getFastestSuccessfulData(urls: string[]): Promise<any> {
  const fetchPromises = urls.map(url =>
    fetch(url).then(res => {
      if (!res.ok) throw new Error(`HTTP Error from ${url}: ${res.status}`);
      return res.json();
    })
  );

  try {
    const data = await Promise.any(fetchPromises);
    console.log('Got data from the fastest successful source:', data);
    return data;
  } catch (error: any) { // Type 'any' for AggregateError compatibility in older TS versions
    if (error instanceof AggregateError) {
      console.error('All data sources failed:', error.errors.map((e: Error) => e.message));
    } else {
      console.error('An unexpected error occurred with Promise.any:', error);
    }
    throw error;
  }
}

// Example: Try fetching from primary, then secondary, then tertiary API endpoints
getFastestSuccessfulData([
  'https://api.primary.example.com/data',
  'https://api.secondary.example.com/data',
  'https://api.tertiary.example.com/data'
]);

Common Pitfalls

  • Forgetting await: If you omit await, the code will continue executing immediately, and you'll be working with a Promise object, not its resolved value. This often leads to TypeError: Cannot read property 'xyz' of undefined or similar issues when trying to access properties of the unresolved Promise.
  • Unnecessary sequential execution: Using await in a loop for independent operations makes them run one after another, wasting time. Use Promise.all for concurrent execution.
  • Unhandled Promise rejections: If an async function rejects and its returned Promise isn't awaited or doesn't have a .catch() handler, it can lead to unhandled promise rejection errors, especially in Node.js environments where this might crash the process by default.
  • Top-level await (Node.js >= 14.8, modern browsers): While now supported in module contexts, you generally can't use await directly at the top level of a script without wrapping it in an async function, especially in older environments or non-module scripts.
  • Swallowing errors: Overuse of try...catch without re-throwing or logging can mask failures, making debugging difficult. Ensure errors are either handled meaningfully or propagated.

Back to the knowledge base · Ask the AI assistant