JavaScript — Async/Await
Async/await is a modern JavaScript syntax for working with asynchronous operations. Introduced in ES2017 (ES8), it builds on top of Promises and provides a way to write asynchronous code that reads like synchronous code — without blocking the main thread.
Before async/await, developers used callbacks (leading to "callback hell") and then Promise chains (.then() and .catch()). While Promises were a significant improvement, they still required building mental models around chaining. Async/await flattens these chains into linear, procedural-style code while preserving all the non-blocking benefits.
Understanding async/await is essential for modern JavaScript development — from API calls and file I/O to database queries and real-time data streams. This page covers everything from the fundamentals to advanced patterns and performance optimization.
| 1 | // The evolution of asynchronous JavaScript |
| 2 | // Callback style (pre-2015) |
| 3 | function fetchUser(id, cb) { |
| 4 | setTimeout(() => cb(null, { id, name: "Alice" }), 1000); |
| 5 | } |
| 6 | fetchUser(1, (err, user) => { |
| 7 | if (err) return console.error(err); |
| 8 | console.log(user.name); |
| 9 | }); |
| 10 | |
| 11 | // Promise style (ES2015) |
| 12 | function fetchUser(id) { |
| 13 | return new Promise((resolve) => { |
| 14 | setTimeout(() => resolve({ id, name: "Alice" }), 1000); |
| 15 | }); |
| 16 | } |
| 17 | fetchUser(1).then(user => console.log(user.name)); |
| 18 | |
| 19 | // Async/await style (ES2017) |
| 20 | async function main() { |
| 21 | const user = await fetchUser(1); |
| 22 | console.log(user.name); |
| 23 | } |
| 24 | main(); |
An async function is a function that implicitly returns a Promise. The await keyword can only be used inside an async function and pauses execution until the awaited Promise settles (resolves or rejects). Despite appearing synchronous, the event loop is not blocked — other code continues to run.
The async Keyword
| 1 | // An async function always returns a Promise |
| 2 | async function greet(name) { |
| 3 | return `Hello, ${name}`; |
| 4 | } |
| 5 | |
| 6 | // The return value is wrapped in a resolved Promise |
| 7 | const result = greet("Alice"); |
| 8 | console.log(result); // Promise { "Hello, Alice" } |
| 9 | console.log(result instanceof Promise); // true |
| 10 | |
| 11 | // To get the value, use await or .then() |
| 12 | console.log(await greet("Bob")); // "Hello, Bob" |
| 13 | greet("Charlie").then(console.log); // "Hello, Charlie" |
| 14 | |
| 15 | // If you don't return anything, the Promise resolves to undefined |
| 16 | async function noop() { |
| 17 | // no return |
| 18 | } |
| 19 | console.log(await noop()); // undefined |
| 20 | |
| 21 | // Async function expression |
| 22 | const getData = async function() { |
| 23 | return await fetchData(); |
| 24 | }; |
| 25 | |
| 26 | // Async arrow function |
| 27 | const getData2 = async () => { |
| 28 | return await fetchData(); |
| 29 | }; |
| 30 | |
| 31 | // Async IIFE — run async code at the top level |
| 32 | (async () => { |
| 33 | const data = await fetch("/api/data"); |
| 34 | console.log(await data.json()); |
| 35 | })(); |
| 36 | |
| 37 | // Async methods in objects and classes |
| 38 | const api = { |
| 39 | async fetch(url) { |
| 40 | const res = await fetch(url); |
| 41 | return res.json(); |
| 42 | }, |
| 43 | }; |
| 44 | |
| 45 | class Database { |
| 46 | async query(sql) { |
| 47 | // simulate query |
| 48 | return [{ id: 1 }]; |
| 49 | } |
| 50 | } |
The await Keyword
| 1 | // await pauses until the Promise settles |
| 2 | async function demo() { |
| 3 | console.log("1. Starting"); |
| 4 | |
| 5 | const result = await new Promise((resolve) => { |
| 6 | setTimeout(() => resolve("2. Done!"), 1000); |
| 7 | }); |
| 8 | |
| 9 | console.log(result); |
| 10 | console.log("3. After await"); |
| 11 | } |
| 12 | |
| 13 | demo(); |
| 14 | // Output (after 1 second): |
| 15 | // 1. Starting |
| 16 | // 2. Done! |
| 17 | // 3. After await |
| 18 | |
| 19 | // await works with any "thenable" (object with .then method) |
| 20 | class Thenable { |
| 21 | constructor(value) { |
| 22 | this.value = value; |
| 23 | } |
| 24 | then(resolve) { |
| 25 | setTimeout(() => resolve(this.value), 500); |
| 26 | } |
| 27 | } |
| 28 | |
| 29 | async function example() { |
| 30 | const val = await new Thenable("works!"); |
| 31 | console.log(val); // "works!" (after 500ms) |
| 32 | } |
| 33 | |
| 34 | // await also works with non-Promise values |
| 35 | async function mixed() { |
| 36 | const a = await 42; // wraps in Promise.resolve(42) |
| 37 | const b = await "hello"; // wraps in Promise.resolve("hello") |
| 38 | const c = await { key: "val" }; // wraps in Promise.resolve({ key: "val" }) |
| 39 | console.log(a, b, c); // 42 "hello" { key: "val" } |
| 40 | } |
| 41 | |
| 42 | // You can await any expression, not just a variable |
| 43 | async function inline() { |
| 44 | const data = await fetch("/api/users").then(r => r.json()); |
| 45 | const ids = await Promise.all([fetch(1), fetch(2), fetch(3)]); |
| 46 | const result = await (await fetch("/api/calc")).json(); |
| 47 | } |
info
Promise vs Async/Await Comparison
| 1 | // Same operation: Promise chain vs async/await |
| 2 | |
| 3 | // Promise chain approach |
| 4 | function loadUserProfile(userId) { |
| 5 | return fetch(`/api/users/${userId}`) |
| 6 | .then(res => { |
| 7 | if (!res.ok) throw new Error("Failed to fetch user"); |
| 8 | return res.json(); |
| 9 | }) |
| 10 | .then(user => { |
| 11 | return fetch(`/api/posts?userId=${user.id}`); |
| 12 | }) |
| 13 | .then(res => res.json()) |
| 14 | .then(posts => { |
| 15 | return fetch(`/api/comments?postIds=${posts.map(p => p.id).join(",")}`); |
| 16 | }) |
| 17 | .then(res => res.json()) |
| 18 | .catch(err => { |
| 19 | console.error("Error:", err); |
| 20 | throw err; |
| 21 | }); |
| 22 | } |
| 23 | |
| 24 | // Async/await approach — reads linearly |
| 25 | async function loadUserProfile(userId) { |
| 26 | try { |
| 27 | const userRes = await fetch(`/api/users/${userId}`); |
| 28 | if (!userRes.ok) throw new Error("Failed to fetch user"); |
| 29 | const user = await userRes.json(); |
| 30 | |
| 31 | const postsRes = await fetch(`/api/posts?userId=${user.id}`); |
| 32 | const posts = await postsRes.json(); |
| 33 | |
| 34 | const commentsRes = await fetch( |
| 35 | `/api/comments?postIds=${posts.map(p => p.id).join(",")}` |
| 36 | ); |
| 37 | const comments = await commentsRes.json(); |
| 38 | |
| 39 | return { user, posts, comments }; |
| 40 | } catch (err) { |
| 41 | console.error("Error:", err); |
| 42 | throw err; |
| 43 | } |
| 44 | } |
| 45 | |
| 46 | // Both return a Promise — consumers use them the same way |
| 47 | loadUserProfile(42).then(profile => { |
| 48 | console.log(profile.user.name, profile.posts.length); |
| 49 | }); |
One of the biggest advantages of async/await is that it brings asynchronous error handling into the familiar try/catch paradigm. Instead of .catch() chains or unhandled Promise rejections, you use the same error handling syntax as synchronous code.
| 1 | // Basic try/catch with async/await |
| 2 | async function fetchData(url) { |
| 3 | try { |
| 4 | const response = await fetch(url); |
| 5 | if (!response.ok) { |
| 6 | throw new Error(`HTTP Error: ${response.status}`); |
| 7 | } |
| 8 | return await response.json(); |
| 9 | } catch (error) { |
| 10 | console.error("Fetch failed:", error.message); |
| 11 | throw error; // re-throw if caller needs to handle it |
| 12 | } |
| 13 | } |
| 14 | |
| 15 | // Multiple awaits in a single try/catch |
| 16 | async function complexOperation() { |
| 17 | try { |
| 18 | const user = await fetchUser(); |
| 19 | const posts = await fetchPosts(user.id); |
| 20 | const comments = await fetchComments(posts.map(p => p.id)); |
| 21 | return { user, posts, comments }; |
| 22 | } catch (error) { |
| 23 | console.error("Operation failed at some step:", error); |
| 24 | // Log to error tracking service |
| 25 | await logError(error); |
| 26 | // Return a sensible default |
| 27 | return { user: null, posts: [], comments: [] }; |
| 28 | } |
| 29 | } |
| 30 | |
| 31 | // Selective error handling — per-operation try/catch |
| 32 | async function selectiveHandling() { |
| 33 | const user = await fetchUser().catch(e => { |
| 34 | console.warn("Could not fetch user, using default"); |
| 35 | return { id: 0, name: "Guest" }; |
| 36 | }); |
| 37 | |
| 38 | let posts; |
| 39 | try { |
| 40 | posts = await fetchPosts(user.id); |
| 41 | } catch { |
| 42 | posts = []; |
| 43 | } |
| 44 | |
| 45 | return { user, posts }; |
| 46 | } |
| 47 | |
| 48 | // Error handling with finally — cleanup |
| 49 | async function withCleanup() { |
| 50 | const conn = await openDatabase(); |
| 51 | try { |
| 52 | const data = await conn.query("SELECT * FROM users"); |
| 53 | return data; |
| 54 | } catch (error) { |
| 55 | console.error("Query failed:", error); |
| 56 | throw error; |
| 57 | } finally { |
| 58 | await conn.close(); // always runs |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | // The error.stack property is preserved |
| 63 | async function stackTraceDemo() { |
| 64 | try { |
| 65 | await Promise.reject(new Error("Something broke")); |
| 66 | } catch (error) { |
| 67 | console.log(error.message); // "Something broke" |
| 68 | console.log(error.stack); // Full stack trace including async context |
| 69 | } |
| 70 | } |
danger
The Error Boundary Pattern
| 1 | // Wrapper pattern — ensures errors never go unhandled |
| 2 | async function safeAsync(asyncFn, fallback = null) { |
| 3 | try { |
| 4 | return await asyncFn(); |
| 5 | } catch (error) { |
| 6 | console.error("Async operation failed:", error); |
| 7 | return fallback; |
| 8 | } |
| 9 | } |
| 10 | |
| 11 | // Usage |
| 12 | const data = await safeAsync(() => fetch("/api/data").then(r => r.json()), []); |
| 13 | |
| 14 | // Higher-order function for error-wrapping |
| 15 | function withErrorHandling(asyncFn, onError) { |
| 16 | return async function(...args) { |
| 17 | try { |
| 18 | return await asyncFn.apply(this, args); |
| 19 | } catch (error) { |
| 20 | if (onError) { |
| 21 | return onError(error, ...args); |
| 22 | } |
| 23 | console.error(`Async function failed: ${asyncFn.name}`, error); |
| 24 | throw error; |
| 25 | } |
| 26 | }; |
| 27 | } |
| 28 | |
| 29 | const safeFetch = withErrorHandling( |
| 30 | async (url) => { |
| 31 | const res = await fetch(url); |
| 32 | if (!res.ok) throw new Error(`Status ${res.status}`); |
| 33 | return res.json(); |
| 34 | }, |
| 35 | (err, url) => { |
| 36 | console.warn(`Failed to fetch ${url}, returning empty`); |
| 37 | return {}; |
| 38 | } |
| 39 | ); |
| 40 | |
| 41 | // Custom error classes for async operations |
| 42 | class NetworkError extends Error { |
| 43 | constructor(message, statusCode) { |
| 44 | super(message); |
| 45 | this.name = "NetworkError"; |
| 46 | this.statusCode = statusCode; |
| 47 | } |
| 48 | } |
| 49 | |
| 50 | class ValidationError extends Error { |
| 51 | constructor(field, message) { |
| 52 | super(message); |
| 53 | this.name = "ValidationError"; |
| 54 | this.field = field; |
| 55 | } |
| 56 | } |
| 57 | |
| 58 | async function apiCall(endpoint) { |
| 59 | const res = await fetch(endpoint); |
| 60 | if (res.status === 400) { |
| 61 | const body = await res.json(); |
| 62 | throw new ValidationError(body.field, body.message); |
| 63 | } |
| 64 | if (!res.ok) { |
| 65 | throw new NetworkError("Request failed", res.status); |
| 66 | } |
| 67 | return res.json(); |
| 68 | } |
A common mistake with async/await is accidentally serializing operations that could run in parallel. Understanding when to await sequentially and when to use Promise.all() is critical for performance.
Sequential Execution
| 1 | // Sequential — each await waits for the previous |
| 2 | // Use when operations depend on each other |
| 3 | async function sequential() { |
| 4 | const user = await fetchUser(); // 1s |
| 5 | const posts = await fetchPosts(user.id); // depends on user → 1s |
| 6 | const comments = await fetchComments(posts[0].id); // depends on posts → 1s |
| 7 | // Total: ~3 seconds |
| 8 | return { user, posts, comments }; |
| 9 | } |
| 10 | |
| 11 | // Sequential in a loop — one at a time |
| 12 | async function processItemsSequentially(items) { |
| 13 | const results = []; |
| 14 | for (const item of items) { |
| 15 | const result = await processItem(item); // waits for each |
| 16 | results.push(result); |
| 17 | } |
| 18 | return results; |
| 19 | } |
| 20 | |
| 21 | // Using reduce for sequential Promise chaining |
| 22 | async function sequentialReduce(items) { |
| 23 | return items.reduce(async (prevPromise, item) => { |
| 24 | const results = await prevPromise; |
| 25 | const result = await processItem(item); |
| 26 | return [...results, result]; |
| 27 | }, Promise.resolve([])); |
| 28 | } |
Parallel Execution with Promise.all
| 1 | // Parallel — start all operations at once, await all results |
| 2 | // Use when operations are independent |
| 3 | |
| 4 | async function parallel() { |
| 5 | const [users, posts, comments] = await Promise.all([ |
| 6 | fetchUsers(), // 1s |
| 7 | fetchPosts(), // 1s (runs concurrently with fetchUsers) |
| 8 | fetchComments(), // 1s (runs concurrently with both) |
| 9 | ]); |
| 10 | // Total: ~1 second (not 3!) |
| 11 | return { users, posts, comments }; |
| 12 | } |
| 13 | |
| 14 | // Mapping to an array of Promises, then awaiting all |
| 15 | async function processItemsInParallel(items) { |
| 16 | const promises = items.map(item => processItem(item)); |
| 17 | const results = await Promise.all(promises); |
| 18 | return results; |
| 19 | } |
| 20 | |
| 21 | // Promise.allSettled — wait for all, even if some reject |
| 22 | async function parallelWithPartialFailure() { |
| 23 | const results = await Promise.allSettled([ |
| 24 | fetch("/api/a"), |
| 25 | fetch("/api/b"), |
| 26 | fetch("/api/c"), |
| 27 | ]); |
| 28 | |
| 29 | const fulfilled = results |
| 30 | .filter(r => r.status === "fulfilled") |
| 31 | .map(r => r.value); |
| 32 | |
| 33 | const rejected = results |
| 34 | .filter(r => r.status === "rejected") |
| 35 | .map(r => r.reason); |
| 36 | |
| 37 | return { fulfilled, rejected }; |
| 38 | } |
| 39 | |
| 40 | // Promise.race — use the first settled result |
| 41 | async function raceExample() { |
| 42 | const result = await Promise.race([ |
| 43 | fetch("/api/data"), |
| 44 | timeout(5000), // reject after 5s |
| 45 | ]); |
| 46 | return result; |
| 47 | } |
| 48 | |
| 49 | // Promise.any — use the first fulfilled result (ignore rejections) |
| 50 | async function anyExample() { |
| 51 | const result = await Promise.any([ |
| 52 | fetch("/api/server1"), |
| 53 | fetch("/api/server2"), |
| 54 | fetch("/api/server3"), |
| 55 | ]); |
| 56 | return result; // fastest successful response |
| 57 | } |
best practice
Concurrency Control with Batching
| 1 | // Batch processing — limit concurrent operations |
| 2 | async function processInBatches(items, batchSize = 5, fn) { |
| 3 | const results = []; |
| 4 | for (let i = 0; i < items.length; i += batchSize) { |
| 5 | const batch = items.slice(i, i + batchSize); |
| 6 | const batchResults = await Promise.all(batch.map(item => fn(item))); |
| 7 | results.push(...batchResults); |
| 8 | } |
| 9 | return results; |
| 10 | } |
| 11 | |
| 12 | // Concurrency limiter pattern |
| 13 | async function concurrencyLimit(items, limit, fn) { |
| 14 | const results = []; |
| 15 | const executing = new Set(); |
| 16 | |
| 17 | for (const item of items) { |
| 18 | const promise = fn(item).then(result => { |
| 19 | executing.delete(promise); |
| 20 | return result; |
| 21 | }); |
| 22 | executing.add(promise); |
| 23 | results.push(promise); |
| 24 | |
| 25 | if (executing.size >= limit) { |
| 26 | await Promise.race(executing); |
| 27 | } |
| 28 | } |
| 29 | |
| 30 | return Promise.all(results); |
| 31 | } |
| 32 | |
| 33 | // Usage |
| 34 | const urls = Array.from({ length: 100 }, (_, i) => `/api/item/${i}`); |
| 35 | const data = await concurrencyLimit(urls, 10, async (url) => { |
| 36 | const res = await fetch(url); |
| 37 | return res.json(); |
| 38 | }); |
| 39 | // At most 10 concurrent requests at any time |
Beyond basic usage, several powerful patterns emerge when combining async/await with other JavaScript features. These patterns handle real-world concerns like retrying failed operations, timeouts, cancellation, and streaming.
Retry Pattern
| 1 | // Retry an async operation with exponential backoff |
| 2 | async function retry(fn, options = {}) { |
| 3 | const { |
| 4 | maxRetries = 3, |
| 5 | baseDelay = 1000, |
| 6 | maxDelay = 30000, |
| 7 | shouldRetry = (err) => true, |
| 8 | } = options; |
| 9 | |
| 10 | let lastError; |
| 11 | |
| 12 | for (let attempt = 0; attempt <= maxRetries; attempt++) { |
| 13 | try { |
| 14 | return await fn(); |
| 15 | } catch (error) { |
| 16 | lastError = error; |
| 17 | if (attempt === maxRetries || !shouldRetry(error)) { |
| 18 | throw error; |
| 19 | } |
| 20 | // Calculate delay with exponential backoff + jitter |
| 21 | const delay = Math.min( |
| 22 | baseDelay * Math.pow(2, attempt) + Math.random() * 1000, |
| 23 | maxDelay |
| 24 | ); |
| 25 | console.warn(`Attempt ${attempt + 1} failed, retrying in ${delay}ms...`); |
| 26 | await new Promise(resolve => setTimeout(resolve, delay)); |
| 27 | } |
| 28 | } |
| 29 | |
| 30 | throw lastError; |
| 31 | } |
| 32 | |
| 33 | // Usage |
| 34 | const data = await retry( |
| 35 | () => fetch("/api/unstable").then(r => r.json()), |
| 36 | { maxRetries: 5, baseDelay: 500 } |
| 37 | ); |
| 38 | |
| 39 | // Selective retry — only for certain errors |
| 40 | const safeData = await retry( |
| 41 | () => fetch("/api/data").then(r => { |
| 42 | if (r.status === 429) throw new Error("Rate limited"); |
| 43 | if (r.status >= 500) throw new Error("Server error"); |
| 44 | return r.json(); |
| 45 | }), |
| 46 | { |
| 47 | maxRetries: 3, |
| 48 | shouldRetry: (err) => { |
| 49 | return err.message === "Rate limited" |
| 50 | || err.message === "Server error"; |
| 51 | }, |
| 52 | } |
| 53 | ); |
Timeout Pattern
| 1 | // Wrap an async operation with a timeout |
| 2 | function withTimeout(promise, ms, errorMessage = "Operation timed out") { |
| 3 | let timer; |
| 4 | const timeoutPromise = new Promise((_, reject) => { |
| 5 | timer = setTimeout(() => reject(new Error(errorMessage)), ms); |
| 6 | }); |
| 7 | return Promise.race([promise, timeoutPromise]) |
| 8 | .finally(() => clearTimeout(timer)); |
| 9 | } |
| 10 | |
| 11 | // Usage |
| 12 | async function fetchWithTimeout(url, ms = 5000) { |
| 13 | const controller = new AbortController(); |
| 14 | const timeoutId = setTimeout(() => controller.abort(), ms); |
| 15 | |
| 16 | try { |
| 17 | const response = await fetch(url, { signal: controller.signal }); |
| 18 | return await response.json(); |
| 19 | } catch (error) { |
| 20 | if (error.name === "AbortError") { |
| 21 | throw new Error(`Request to ${url} timed out after ${ms}ms`); |
| 22 | } |
| 23 | throw error; |
| 24 | } finally { |
| 25 | clearTimeout(timeoutId); |
| 26 | } |
| 27 | } |
| 28 | |
| 29 | // Timeout with fallback value |
| 30 | async function fetchWithFallback(url, ms = 3000, fallback = {}) { |
| 31 | try { |
| 32 | return await withTimeout(fetch(url).then(r => r.json()), ms); |
| 33 | } catch { |
| 34 | console.warn(`Fetch failed/timed out, using fallback`); |
| 35 | return fallback; |
| 36 | } |
| 37 | } |
| 38 | |
| 39 | // Timeout wrapper for any promise |
| 40 | function timeout(ms) { |
| 41 | return new Promise((_, reject) => |
| 42 | setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms) |
| 43 | ); |
| 44 | } |
| 45 | |
| 46 | async function robustFetch(url) { |
| 47 | return Promise.race([ |
| 48 | fetch(url).then(r => r.json()), |
| 49 | timeout(5000), |
| 50 | ]); |
| 51 | } |
Cancellation Pattern
| 1 | // Async cancellation using AbortController |
| 2 | class AsyncTask { |
| 3 | constructor() { |
| 4 | this.controller = new AbortController(); |
| 5 | } |
| 6 | |
| 7 | cancel() { |
| 8 | this.controller.abort(); |
| 9 | } |
| 10 | |
| 11 | async run(url) { |
| 12 | const signal = this.controller.signal; |
| 13 | try { |
| 14 | const res = await fetch(url, { signal }); |
| 15 | const data = await res.json(); |
| 16 | |
| 17 | // Check if cancelled after fetch |
| 18 | if (signal.aborted) { |
| 19 | throw new Error("Task cancelled"); |
| 20 | } |
| 21 | |
| 22 | // Simulate more async work |
| 23 | await this.processData(data, signal); |
| 24 | return data; |
| 25 | } catch (error) { |
| 26 | if (error.name === "AbortError") { |
| 27 | console.log("Task was cancelled"); |
| 28 | return null; |
| 29 | } |
| 30 | throw error; |
| 31 | } |
| 32 | } |
| 33 | |
| 34 | async processData(data, signal) { |
| 35 | for (const item of data) { |
| 36 | if (signal.aborted) break; |
| 37 | await this.processItem(item); |
| 38 | } |
| 39 | } |
| 40 | |
| 41 | async processItem(item) { |
| 42 | return new Promise(resolve => setTimeout(resolve, 100)); |
| 43 | } |
| 44 | } |
| 45 | |
| 46 | // Usage |
| 47 | const task = new AsyncTask(); |
| 48 | setTimeout(() => task.cancel(), 2000); // cancel after 2s |
| 49 | const result = await task.run("/api/large-data"); |
| 50 | |
| 51 | // Cancellable sleep |
| 52 | function cancellableSleep(ms, signal) { |
| 53 | return new Promise((resolve, reject) => { |
| 54 | if (signal.aborted) { |
| 55 | return reject(new DOMException("Aborted", "AbortError")); |
| 56 | } |
| 57 | const timer = setTimeout(resolve, ms); |
| 58 | signal.addEventListener("abort", () => { |
| 59 | clearTimeout(timer); |
| 60 | reject(new DOMException("Aborted", "AbortError")); |
| 61 | }, { once: true }); |
| 62 | }); |
| 63 | } |
| 64 | |
| 65 | async function demo() { |
| 66 | const ac = new AbortController(); |
| 67 | setTimeout(() => ac.abort(), 500); |
| 68 | try { |
| 69 | await cancellableSleep(2000, ac.signal); |
| 70 | console.log("Done"); |
| 71 | } catch { |
| 72 | console.log("Slept"); |
| 73 | } |
| 74 | } |
Async Iteration & Streaming
| 1 | // Async iterators — process data as it arrives |
| 2 | async function* paginatedFetch(baseUrl, maxPages = 10) { |
| 3 | let page = 1; |
| 4 | let hasMore = true; |
| 5 | |
| 6 | while (hasMore && page <= maxPages) { |
| 7 | const res = await fetch(`${baseUrl}?page=${page}`); |
| 8 | const data = await res.json(); |
| 9 | |
| 10 | yield data.items; |
| 11 | |
| 12 | hasMore = data.hasMore; |
| 13 | page++; |
| 14 | } |
| 15 | } |
| 16 | |
| 17 | // Consume async iterator |
| 18 | async function consumeAll() { |
| 19 | const results = []; |
| 20 | for await (const items of paginatedFetch("/api/users")) { |
| 21 | results.push(...items); |
| 22 | console.log(`Fetched ${items.length} items, total: ${results.length}`); |
| 23 | } |
| 24 | return results; |
| 25 | } |
| 26 | |
| 27 | // Async generator for streaming data |
| 28 | async function* streamLines(url) { |
| 29 | const response = await fetch(url); |
| 30 | const reader = response.body.getReader(); |
| 31 | const decoder = new TextDecoder(); |
| 32 | let buffer = ""; |
| 33 | |
| 34 | while (true) { |
| 35 | const { done, value } = await reader.read(); |
| 36 | if (done) break; |
| 37 | |
| 38 | buffer += decoder.decode(value, { stream: true }); |
| 39 | const lines = buffer.split("\n"); |
| 40 | buffer = lines.pop() || ""; |
| 41 | |
| 42 | for (const line of lines) { |
| 43 | if (line.trim()) yield line; |
| 44 | } |
| 45 | } |
| 46 | |
| 47 | if (buffer.trim()) yield buffer; |
| 48 | } |
| 49 | |
| 50 | // Consume streaming data |
| 51 | async function processStream() { |
| 52 | for await (const line of streamLines("/api/events")) { |
| 53 | const event = JSON.parse(line); |
| 54 | console.log("Event:", event); |
| 55 | if (event.type === "complete") break; |
| 56 | } |
| 57 | } |
| 58 | |
| 59 | // Async queue pattern — producer/consumer |
| 60 | class AsyncQueue { |
| 61 | constructor() { |
| 62 | this.items = []; |
| 63 | this.resolveQueue = []; |
| 64 | } |
| 65 | |
| 66 | enqueue(item) { |
| 67 | if (this.resolveQueue.length > 0) { |
| 68 | const resolve = this.resolveQueue.shift(); |
| 69 | resolve(item); |
| 70 | } else { |
| 71 | this.items.push(item); |
| 72 | } |
| 73 | } |
| 74 | |
| 75 | async dequeue() { |
| 76 | if (this.items.length > 0) { |
| 77 | return this.items.shift(); |
| 78 | } |
| 79 | return new Promise(resolve => { |
| 80 | this.resolveQueue.push(resolve); |
| 81 | }); |
| 82 | } |
| 83 | } |
| 84 | |
| 85 | // Usage: producer adds items, consumer processes them |
| 86 | const queue = new AsyncQueue(); |
| 87 | setInterval(() => queue.enqueue(Date.now()), 1000); |
| 88 | |
| 89 | async function consumer() { |
| 90 | while (true) { |
| 91 | const item = await queue.dequeue(); |
| 92 | console.log("Processed:", item); |
| 93 | } |
| 94 | } |
pro tip
Async/await integrates well with classes, but constructors cannot be async. Instead, use the factory pattern or initialize after construction.
| 1 | // Async factory pattern — avoid async constructors |
| 2 | class DatabaseConnection { |
| 3 | constructor(config) { |
| 4 | this.config = config; |
| 5 | this.connected = false; |
| 6 | } |
| 7 | |
| 8 | // Factory method instead of async constructor |
| 9 | static async create(config) { |
| 10 | const conn = new DatabaseConnection(config); |
| 11 | await conn.connect(); |
| 12 | return conn; |
| 13 | } |
| 14 | |
| 15 | async connect() { |
| 16 | // Simulate connection |
| 17 | await new Promise(r => setTimeout(r, 500)); |
| 18 | this.connected = true; |
| 19 | console.log("Connected"); |
| 20 | } |
| 21 | |
| 22 | async query(sql) { |
| 23 | if (!this.connected) throw new Error("Not connected"); |
| 24 | // Simulate query |
| 25 | await new Promise(r => setTimeout(r, 100)); |
| 26 | return [{ result: "data" }]; |
| 27 | } |
| 28 | |
| 29 | async disconnect() { |
| 30 | if (this.connected) { |
| 31 | await new Promise(r => setTimeout(r, 200)); |
| 32 | this.connected = false; |
| 33 | } |
| 34 | } |
| 35 | } |
| 36 | |
| 37 | // Usage |
| 38 | const db = await DatabaseConnection.create({ host: "localhost" }); |
| 39 | const data = await db.query("SELECT * FROM users"); |
| 40 | await db.disconnect(); |
| 41 | |
| 42 | // Init method pattern |
| 43 | class Service { |
| 44 | constructor() { |
| 45 | this.initialized = false; |
| 46 | this.initPromise = null; |
| 47 | } |
| 48 | |
| 49 | async init() { |
| 50 | if (this.initialized) return; |
| 51 | if (this.initPromise) return this.initPromise; |
| 52 | |
| 53 | this.initPromise = this._doInit(); |
| 54 | return this.initPromise; |
| 55 | } |
| 56 | |
| 57 | async _doInit() { |
| 58 | await new Promise(r => setTimeout(r, 300)); |
| 59 | this.initialized = true; |
| 60 | console.log("Service initialized"); |
| 61 | } |
| 62 | |
| 63 | async doWork() { |
| 64 | await this.init(); // ensures init before work |
| 65 | console.log("Doing work"); |
| 66 | } |
| 67 | } |
| 68 | |
| 69 | // Async getter pattern |
| 70 | class Config { |
| 71 | constructor(url) { |
| 72 | this.url = url; |
| 73 | this._cache = null; |
| 74 | } |
| 75 | |
| 76 | get data() { |
| 77 | if (!this._cache) { |
| 78 | this._cache = this._load(); |
| 79 | } |
| 80 | return this._cache; |
| 81 | } |
| 82 | |
| 83 | async _load() { |
| 84 | const res = await fetch(this.url); |
| 85 | return res.json(); |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | const config = new Config("/api/config"); |
| 90 | const data = await config.data; // awaits transparently |
| 91 | console.log(data); |
Async/await performance is generally excellent — the overhead compared to native Promises is minimal. However, certain patterns can hurt performance. Understanding the microtask queue, avoiding unnecessary await, and measuring correctly are important.
| 1 | // Don't await unnecessarily — let Promises be Promises |
| 2 | // BAD: awaiting when you don't need the value yet |
| 3 | async function slow() { |
| 4 | const a = await fetchA(); // starts and waits |
| 5 | const b = await fetchB(); // starts only after A finishes |
| 6 | return a + b; |
| 7 | } |
| 8 | |
| 9 | // GOOD: start all tasks, await results |
| 10 | async function fast() { |
| 11 | const aPromise = fetchA(); // starts immediately |
| 12 | const bPromise = fetchB(); // starts immediately (parallel) |
| 13 | const a = await aPromise; |
| 14 | const b = await bPromise; |
| 15 | return a + b; |
| 16 | } |
| 17 | |
| 18 | // BETTER: Use Promise.all |
| 19 | async function fastest() { |
| 20 | const [a, b] = await Promise.all([fetchA(), fetchB()]); |
| 21 | return a + b; |
| 22 | } |
| 23 | |
| 24 | // Avoid await in hot loops |
| 25 | // BAD: sequential in a loop |
| 26 | async function badLoop(items) { |
| 27 | const results = []; |
| 28 | for (const item of items) { |
| 29 | results.push(await process(item)); // one at a time |
| 30 | } |
| 31 | return results; |
| 32 | } |
| 33 | |
| 34 | // GOOD: parallel processing |
| 35 | async function goodLoop(items) { |
| 36 | return Promise.all(items.map(item => process(item))); |
| 37 | } |
| 38 | |
| 39 | // If you need concurrency limiting: |
| 40 | async function batchedLoop(items, batchSize = 5) { |
| 41 | const results = []; |
| 42 | for (let i = 0; i < items.length; i += batchSize) { |
| 43 | const batch = items.slice(i, i + batchSize); |
| 44 | const batchResults = await Promise.all( |
| 45 | batch.map(item => process(item)) |
| 46 | ); |
| 47 | results.push(...batchResults); |
| 48 | } |
| 49 | return results; |
| 50 | } |
| 51 | |
| 52 | // Unnecessary async wrappers add overhead |
| 53 | // BAD: wrapper that does nothing async |
| 54 | async function getValue() { |
| 55 | return await someValue; // unnecessary await + async |
| 56 | } |
| 57 | |
| 58 | // GOOD: let the caller decide |
| 59 | function getValue() { |
| 60 | return someValue; |
| 61 | } |
| 62 | |
| 63 | // Measuring async performance |
| 64 | async function measure(fn, label = "Operation") { |
| 65 | const start = performance.now(); |
| 66 | const result = await fn(); |
| 67 | const duration = performance.now() - start; |
| 68 | console.log(`${label}: ${duration.toFixed(2)}ms`); |
| 69 | return result; |
| 70 | } |
| 71 | |
| 72 | // Microtask timing — await yields to microtasks |
| 73 | async function microtaskDemo() { |
| 74 | console.log("1"); // sync |
| 75 | |
| 76 | await Promise.resolve(); // yields to microtask queue |
| 77 | |
| 78 | console.log("3"); // resumes after microtasks |
| 79 | |
| 80 | // Queue a microtask |
| 81 | Promise.resolve().then(() => console.log("2.5")); |
| 82 | |
| 83 | await Promise.resolve(); |
| 84 | console.log("4"); |
| 85 | } |
warning
Introduced in ES2022, top-level await allows using await outside of async functions — but only in JavaScript modules (<script type="module"> or .mjs files). This eliminates the need for async IIFE wrappers at the module level.
| 1 | // Top-level await — only works in ES modules |
| 2 | // File: config.mjs |
| 3 | |
| 4 | // Wait for configuration before module is fully loaded |
| 5 | const response = await fetch("/api/config"); |
| 6 | export const config = await response.json(); |
| 7 | |
| 8 | // Dynamic imports with top-level await |
| 9 | const lodash = await import("lodash"); |
| 10 | export const version = lodash.VERSION; |
| 11 | |
| 12 | // Module loading order with top-level await |
| 13 | // module-a.mjs |
| 14 | export const data = await fetch("/api/a").then(r => r.json()); |
| 15 | console.log("Module A loaded"); |
| 16 | |
| 17 | // module-b.mjs — imports from module-a |
| 18 | import { data } from "./module-a.mjs"; |
| 19 | // This module waits for module-a to fully initialize |
| 20 | export const processed = data.map(item => item.value); |
| 21 | console.log("Module B loaded"); |
| 22 | |
| 23 | // Fallback pattern |
| 24 | let locale; |
| 25 | try { |
| 26 | const res = await fetch("/api/locale"); |
| 27 | locale = await res.json(); |
| 28 | } catch { |
| 29 | locale = { lang: "en", region: "US" }; |
| 30 | } |
| 31 | export { locale }; |
| 32 | |
| 33 | // Top-level await blocks sibling modules from executing |
| 34 | // Use cautiously — it delays module evaluation |
| 35 | // GOOD for: configuration, initialization, connection setup |
| 36 | // BAD for: user-facing latency in critical path |
| 37 | |
| 38 | // Dynamic behavior based on async condition |
| 39 | export const db = await (async () => { |
| 40 | const config = await loadConfig(); |
| 41 | if (config.dbType === "mongo") { |
| 42 | return connectToMongo(config.mongoUrl); |
| 43 | } |
| 44 | return connectToPostgres(config.pgUrl); |
| 45 | })(); |
| 46 | |
| 47 | // Tree-shaking note: top-level await prevents tree-shaking |
| 48 | // because modules have side effects (async execution) |
| 49 | // Structure your code so leaf modules use top-level await, |
| 50 | // not your main application bundle |
note
Even experienced developers encounter these async/await pitfalls. Understanding them will save you hours of debugging.
| 1 | // Pitfall 1: Forgetting to await |
| 2 | async function pitfall1() { |
| 3 | const user = fetchUser(); // forgot await — user is a Promise, not data |
| 4 | console.log(user.name); // undefined — Promise objects don't have .name |
| 5 | const actualUser = await user; |
| 6 | console.log(actualUser.name); // correct |
| 7 | } |
| 8 | |
| 9 | // Pitfall 2: Sequential when you meant parallel |
| 10 | async function pitfall2() { |
| 11 | // These run sequentially — slower |
| 12 | const a = await fetch("https://api.example.com/a"); |
| 13 | const aData = await a.json(); |
| 14 | |
| 15 | const b = await fetch("https://api.example.com/b"); |
| 16 | const bData = await b.json(); |
| 17 | |
| 18 | // Better: run in parallel |
| 19 | const [aRes, bRes] = await Promise.all([ |
| 20 | fetch("https://api.example.com/a"), |
| 21 | fetch("https://api.example.com/b"), |
| 22 | ]); |
| 23 | const [aData2, bData2] = await Promise.all([ |
| 24 | aRes.json(), |
| 25 | bRes.json(), |
| 26 | ]); |
| 27 | } |
| 28 | |
| 29 | // Pitfall 3: No error handling |
| 30 | async function pitfall3() { |
| 31 | // If this rejects, the error is an unhandled rejection |
| 32 | await fetch("/api/critical"); |
| 33 | } |
| 34 | // Always handle errors! |
| 35 | async function fixed3() { |
| 36 | try { |
| 37 | await fetch("/api/critical"); |
| 38 | } catch (error) { |
| 39 | console.error("Critical fetch failed:", error); |
| 40 | // Implement fallback |
| 41 | } |
| 42 | } |
| 43 | |
| 44 | // Pitfall 4: Promise.all with mixed failure modes |
| 45 | async function pitfall4() { |
| 46 | try { |
| 47 | // If ONE fails, ALL are lost |
| 48 | const results = await Promise.all([ |
| 49 | unreliableFetch("/api/a"), |
| 50 | unreliableFetch("/api/b"), |
| 51 | unreliableFetch("/api/c"), |
| 52 | ]); |
| 53 | } catch { |
| 54 | // Everything is lost! |
| 55 | } |
| 56 | |
| 57 | // Better: handle individual failures |
| 58 | const results = await Promise.allSettled([ |
| 59 | unreliableFetch("/api/a"), |
| 60 | unreliableFetch("/api/b"), |
| 61 | unreliableFetch("/api/c"), |
| 62 | ]); |
| 63 | const successes = results |
| 64 | .filter(r => r.status === "fulfilled") |
| 65 | .map(r => r.value); |
| 66 | } |
| 67 | |
| 68 | // Pitfall 5: Async callbacks with forEach |
| 69 | async function pitfall5() { |
| 70 | const items = [1, 2, 3, 4, 5]; |
| 71 | |
| 72 | // BAD: forEach does NOT await async callbacks |
| 73 | items.forEach(async (item) => { |
| 74 | await process(item); // fires all at once, no waiting |
| 75 | }); |
| 76 | |
| 77 | // GOOD: use for...of |
| 78 | for (const item of items) { |
| 79 | await process(item); // sequential |
| 80 | } |
| 81 | |
| 82 | // GOOD: parallel with map |
| 83 | await Promise.all(items.map(item => process(item))); |
| 84 | } |
| 85 | |
| 86 | // Pitfall 6: Swallowing errors with .catch() |
| 87 | async function pitfall6() { |
| 88 | await fetch("/api/data").catch(() => { |
| 89 | // Error caught here, returns undefined |
| 90 | // The await above gets undefined, not an error |
| 91 | }); |
| 92 | // Code continues as if nothing happened — may cause downstream failures |
| 93 | } |
| 94 | |
| 95 | // Pitfall 7: Race conditions with shared state |
| 96 | let counter = 0; |
| 97 | async function pitfall7() { |
| 98 | // Multiple calls can interleave |
| 99 | const current = counter; |
| 100 | await someAsyncWork(); |
| 101 | counter = current + 1; // Not atomic! |
| 102 | } |
| 103 | |
| 104 | // Fix with mutex or atomic operations |
| 105 | const mutex = new Mutex(); |
| 106 | async function fixed7() { |
| 107 | await mutex.lock(); |
| 108 | try { |
| 109 | const current = counter; |
| 110 | await someAsyncWork(); |
| 111 | counter = current + 1; |
| 112 | } finally { |
| 113 | mutex.unlock(); |
| 114 | } |
| 115 | } |
Following these best practices will help you write clean, maintainable, and performant async/await code.
best practice