|$ curl https://forge-ai.dev/api/markdown?path=docs/js/async-await
$cat docs/javascript-—-async/await.md
updated This week·35 min read·published

JavaScript — Async/Await

JavaScriptAsync/AwaitAdvancedAdvanced
Introduction

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.

async-evolution.js
JavaScript
1// The evolution of asynchronous JavaScript
2// Callback style (pre-2015)
3function fetchUser(id, cb) {
4 setTimeout(() => cb(null, { id, name: "Alice" }), 1000);
5}
6fetchUser(1, (err, user) => {
7 if (err) return console.error(err);
8 console.log(user.name);
9});
10
11// Promise style (ES2015)
12function fetchUser(id) {
13 return new Promise((resolve) => {
14 setTimeout(() => resolve({ id, name: "Alice" }), 1000);
15 });
16}
17fetchUser(1).then(user => console.log(user.name));
18
19// Async/await style (ES2017)
20async function main() {
21 const user = await fetchUser(1);
22 console.log(user.name);
23}
24main();
Understanding Async/Await

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

async-keyword.js
JavaScript
1// An async function always returns a Promise
2async function greet(name) {
3 return `Hello, ${name}`;
4}
5
6// The return value is wrapped in a resolved Promise
7const result = greet("Alice");
8console.log(result); // Promise { "Hello, Alice" }
9console.log(result instanceof Promise); // true
10
11// To get the value, use await or .then()
12console.log(await greet("Bob")); // "Hello, Bob"
13greet("Charlie").then(console.log); // "Hello, Charlie"
14
15// If you don't return anything, the Promise resolves to undefined
16async function noop() {
17 // no return
18}
19console.log(await noop()); // undefined
20
21// Async function expression
22const getData = async function() {
23 return await fetchData();
24};
25
26// Async arrow function
27const 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
38const api = {
39 async fetch(url) {
40 const res = await fetch(url);
41 return res.json();
42 },
43};
44
45class Database {
46 async query(sql) {
47 // simulate query
48 return [{ id: 1 }];
49 }
50}

The await Keyword

await-keyword.js
JavaScript
1// await pauses until the Promise settles
2async 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
13demo();
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)
20class Thenable {
21 constructor(value) {
22 this.value = value;
23 }
24 then(resolve) {
25 setTimeout(() => resolve(this.value), 500);
26 }
27}
28
29async 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
35async 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
43async 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

While await pauses your async function, it does not block the main thread. The JavaScript engine suspends the function's execution, registers a microtask to resume it when the Promise settles, and continues running other code in the meantime. This is why async/await is "cooperative concurrency" — not true parallelism.

Promise vs Async/Await Comparison

promise-vs-async.js
JavaScript
1// Same operation: Promise chain vs async/await
2
3// Promise chain approach
4function 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
25async 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
47loadUserProfile(42).then(profile => {
48 console.log(profile.user.name, profile.posts.length);
49});
Error Handling with Async/Await

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.

error-handling.js
JavaScript
1// Basic try/catch with async/await
2async 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
16async 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
32async 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
49async 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
63async 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

Unhandled Promise rejections in async functions cause warnings (or crashes in Node.js). If an async function throws and you do not catch it with try/catch or attach a .catch() to the returned Promise, the error becomes an unhandled rejection. Always handle errors at some boundary — either internally with try/catch or externally with .catch().

The Error Boundary Pattern

error-boundary.js
JavaScript
1// Wrapper pattern — ensures errors never go unhandled
2async 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
12const data = await safeAsync(() => fetch("/api/data").then(r => r.json()), []);
13
14// Higher-order function for error-wrapping
15function 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
29const 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
42class NetworkError extends Error {
43 constructor(message, statusCode) {
44 super(message);
45 this.name = "NetworkError";
46 this.statusCode = statusCode;
47 }
48}
49
50class ValidationError extends Error {
51 constructor(field, message) {
52 super(message);
53 this.name = "ValidationError";
54 this.field = field;
55 }
56}
57
58async 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}
Sequential vs Parallel Execution

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

sequential.js
JavaScript
1// Sequential — each await waits for the previous
2// Use when operations depend on each other
3async 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
12async 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
22async 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

parallel.js
JavaScript
1// Parallel — start all operations at once, await all results
2// Use when operations are independent
3
4async 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
15async 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
22async 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
41async 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)
50async 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}
preview

best practice

Use sequential when operations depend on each other's results. Use Promise.all() when operations are independent. For large arrays, consider batching with p-limit or a custom concurrency limiter to avoid overwhelming system resources (e.g., too many open database connections or HTTP sockets).

Concurrency Control with Batching

concurrency-control.js
JavaScript
1// Batch processing — limit concurrent operations
2async 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
13async 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
34const urls = Array.from({ length: 100 }, (_, i) => `/api/item/${i}`);
35const 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
Async Patterns

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

retry-pattern.js
JavaScript
1// Retry an async operation with exponential backoff
2async 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
34const 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
40const 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

timeout-pattern.js
JavaScript
1// Wrap an async operation with a timeout
2function 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
12async 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
30async 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
40function timeout(ms) {
41 return new Promise((_, reject) =>
42 setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms)
43 );
44}
45
46async function robustFetch(url) {
47 return Promise.race([
48 fetch(url).then(r => r.json()),
49 timeout(5000),
50 ]);
51}

Cancellation Pattern

cancellation.js
JavaScript
1// Async cancellation using AbortController
2class 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
47const task = new AsyncTask();
48setTimeout(() => task.cancel(), 2000); // cancel after 2s
49const result = await task.run("/api/large-data");
50
51// Cancellable sleep
52function 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
65async 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

async-iteration.js
JavaScript
1// Async iterators — process data as it arrives
2async 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
18async 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
28async 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
51async 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
60class 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
86const queue = new AsyncQueue();
87setInterval(() => queue.enqueue(Date.now()), 1000);
88
89async function consumer() {
90 while (true) {
91 const item = await queue.dequeue();
92 console.log("Processed:", item);
93 }
94}
🔥

pro tip

Async generators (async function*) and for await...of enable powerful streaming patterns. They are particularly useful for processing large datasets, real-time event streams, paginated APIs, and any scenario where data arrives incrementally. Memory usage stays bounded because you process each chunk as it arrives.
Async in Classes & Constructors

Async/await integrates well with classes, but constructors cannot be async. Instead, use the factory pattern or initialize after construction.

async-classes.js
JavaScript
1// Async factory pattern — avoid async constructors
2class 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
38const db = await DatabaseConnection.create({ host: "localhost" });
39const data = await db.query("SELECT * FROM users");
40await db.disconnect();
41
42// Init method pattern
43class 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
70class 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
89const config = new Config("/api/config");
90const data = await config.data; // awaits transparently
91console.log(data);
Performance & Optimization

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.

async-performance.js
JavaScript
1// Don't await unnecessarily — let Promises be Promises
2// BAD: awaiting when you don't need the value yet
3async 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
10async 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
19async 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
26async 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
35async function goodLoop(items) {
36 return Promise.all(items.map(item => process(item)));
37}
38
39// If you need concurrency limiting:
40async 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
54async function getValue() {
55 return await someValue; // unnecessary await + async
56}
57
58// GOOD: let the caller decide
59function getValue() {
60 return someValue;
61}
62
63// Measuring async performance
64async 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
73async 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

Avoid the "await only at the end" antipattern. If you write return await promise in a try block, the await is necessary so the catch works. But return await promise outside a try/catch is redundant — just return promise. However, some linters (like ESLint's no-return-await) may flag unnecessary awaits for removal.
Top-Level Await

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.

top-level-await.js
JavaScript
1// Top-level await — only works in ES modules
2// File: config.mjs
3
4// Wait for configuration before module is fully loaded
5const response = await fetch("/api/config");
6export const config = await response.json();
7
8// Dynamic imports with top-level await
9const lodash = await import("lodash");
10export const version = lodash.VERSION;
11
12// Module loading order with top-level await
13// module-a.mjs
14export const data = await fetch("/api/a").then(r => r.json());
15console.log("Module A loaded");
16
17// module-b.mjs — imports from module-a
18import { data } from "./module-a.mjs";
19// This module waits for module-a to fully initialize
20export const processed = data.map(item => item.value);
21console.log("Module B loaded");
22
23// Fallback pattern
24let locale;
25try {
26 const res = await fetch("/api/locale");
27 locale = await res.json();
28} catch {
29 locale = { lang: "en", region: "US" };
30}
31export { 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
39export 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

Top-level await is a powerful feature but comes with trade-offs. It blocks the evaluation of importing modules, which can increase startup latency. Use it for initialization that must complete before your module is usable — configuration loading, database connections, etc. Avoid it for user-facing latency-sensitive paths.
Common Pitfalls & Gotchas

Even experienced developers encounter these async/await pitfalls. Understanding them will save you hours of debugging.

common-pitfalls.js
JavaScript
1// Pitfall 1: Forgetting to await
2async 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
10async 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
30async function pitfall3() {
31 // If this rejects, the error is an unhandled rejection
32 await fetch("/api/critical");
33}
34// Always handle errors!
35async 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
45async 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
69async 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()
87async 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
96let counter = 0;
97async 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
105const mutex = new Mutex();
106async 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}
Best Practices

Following these best practices will help you write clean, maintainable, and performant async/await code.

Always use try/catch for error handling in async functions — unhandled rejections crash Node.js processes
Use Promise.all() for independent parallel operations — never serialize what can be parallelized
Use Promise.allSettled() when partial failure is acceptable — don't lose all results because of one failure
Prefer async/await over .then().catch() chains — linear code is easier to read and debug
Name your async functions — stack traces show function names, making debugging easier
Use AbortController for cancellable async operations — especially in UI code and long-running tasks
Avoid top-level await in application entry points — use it only in modules that must initialize before use
Keep async functions focused — each function should do one async operation or coordinate a few
Use async generators and for await...of for streaming data — memory efficient for large datasets
Measure before optimizing — async overhead is usually negligible; focus on parallelism and I/O patterns

best practice

The single most important async/await rule: always handle errors. Every async function should either have an internal try/catch or its caller must handle the rejection. Unhandled Promise rejections are a leading cause of production incidents in Node.js applications. Use ESLint rules like no-floating-promises and require-await to enforce correct patterns.
$Blueprint — Engineering Documentation·Section ID: JS-ASYNC·Revision: 1.0