Docs
README
9.3 Async/Await
Introduction
Async/await is syntactic sugar built on top of Promises, making asynchronous code look and behave more like synchronous code. It was introduced in ES2017 and has become the preferred way to handle async operations.
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā EVOLUTION OF ASYNC JAVASCRIPT ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā ā
ā Callbacks āāāāāāŗ Promises āāāāāāŗ Async/Await ā
ā (1995) (2015) (2017) ā
ā ā
ā callback(err, data) promise.then() async/await ā
ā āāā Nested āāā Chainable āāā Synchronous ā
ā āāā Error handling āāā Better ā style ā
ā ā repetitive ā error flow āāā Try/catch ā
ā āāā "Callback hell" āāā Still some āāā Most readable ā
ā nesting ā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Basic Syntax
Async Functions
// Declaring an async function
async function fetchData() {
return 'data'; // Automatically wrapped in Promise
}
// Arrow function version
const fetchData = async () => {
return 'data';
};
// Method in object
const obj = {
async getData() {
return 'data';
},
};
// Method in class
class DataService {
async fetch() {
return 'data';
}
}
The await Keyword
async function example() {
// await pauses execution until promise resolves
const result = await somePromise;
// Code continues after promise resolves
console.log(result);
}
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā HOW AWAIT WORKS ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā ā
ā async function example() { ā
ā console.log("1. Start"); ā
ā ā
ā const result = await fetch(url); āāā Pauses here ā
ā ā ā
ā ā āāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
ā āāāāŗā Control returns to ā ā
ā ā event loop while waiting ā ā
ā āāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
ā ā
ā console.log("2. Got result"); āāā Resumes when ready ā
ā } ā
ā ā
ā console.log("3. After call"); āāā Runs immediately ā
ā ā
ā Output: 1. Start, 3. After call, 2. Got result ā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Comparing Approaches
Callbacks vs Promises vs Async/Await
// ā Callbacks - nested, repetitive error handling
getUser(id, (err, user) => {
if (err) return handleError(err);
getOrders(user.id, (err, orders) => {
if (err) return handleError(err);
getDetails(orders[0].id, (err, details) => {
if (err) return handleError(err);
console.log(details);
});
});
});
// ā
Promises - flat chain, single catch
getUser(id)
.then((user) => getOrders(user.id))
.then((orders) => getDetails(orders[0].id))
.then((details) => console.log(details))
.catch(handleError);
// ā
ā
Async/Await - reads like synchronous code
async function loadDetails(id) {
try {
const user = await getUser(id);
const orders = await getOrders(user.id);
const details = await getDetails(orders[0].id);
console.log(details);
} catch (error) {
handleError(error);
}
}
Async Function Return Values
Always Returns a Promise
// These are equivalent:
async function getValue() {
return 42;
}
function getValue() {
return Promise.resolve(42);
}
// Usage
getValue().then((v) => console.log(v)); // 42
Throwing Errors
// Throwing in async function = rejected promise
async function willFail() {
throw new Error('Something went wrong');
}
// Same as:
function willFail() {
return Promise.reject(new Error('Something went wrong'));
}
// Both can be caught the same way
willFail().catch((e) => console.error(e.message));
Error Handling
Try/Catch Blocks
async function fetchData() {
try {
const response = await fetch('/api/data');
const data = await response.json();
return data;
} catch (error) {
console.error('Failed to fetch:', error);
throw error; // Re-throw to propagate
}
}
Catching Specific Errors
async function processUser(id) {
try {
const user = await getUser(id);
try {
await validateUser(user);
} catch (validationError) {
console.log('Validation failed, using defaults');
user.role = 'guest';
}
return await saveUser(user);
} catch (error) {
console.error('Process failed:', error);
}
}
Error Handling Patterns
// Pattern 1: Try/catch
async function pattern1() {
try {
const result = await riskyOperation();
return result;
} catch (error) {
return defaultValue;
}
}
// Pattern 2: .catch() on await
async function pattern2() {
const result = await riskyOperation().catch((e) => defaultValue);
return result;
}
// Pattern 3: Tuple return [error, result]
async function safeAwait(promise) {
try {
const result = await promise;
return [null, result];
} catch (error) {
return [error, null];
}
}
async function pattern3() {
const [error, result] = await safeAwait(riskyOperation());
if (error) {
return defaultValue;
}
return result;
}
Sequential vs Parallel
Sequential Execution
// Each await waits for the previous one
async function sequential() {
const start = Date.now();
const a = await delay(100, 'A'); // Wait 100ms
const b = await delay(100, 'B'); // Wait another 100ms
const c = await delay(100, 'C'); // Wait another 100ms
console.log(`Total: ${Date.now() - start}ms`); // ~300ms
return [a, b, c];
}
Parallel Execution
// Start all promises at once
async function parallel() {
const start = Date.now();
// Start all at once
const promiseA = delay(100, 'A');
const promiseB = delay(100, 'B');
const promiseC = delay(100, 'C');
// Wait for all
const a = await promiseA;
const b = await promiseB;
const c = await promiseC;
console.log(`Total: ${Date.now() - start}ms`); // ~100ms
return [a, b, c];
}
// Or use Promise.all
async function parallelWithAll() {
const [a, b, c] = await Promise.all([
delay(100, 'A'),
delay(100, 'B'),
delay(100, 'C'),
]);
return [a, b, c];
}
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā SEQUENTIAL vs PARALLEL ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā ā
ā SEQUENTIAL (one after another) ā
ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
ā |ā Task A ā|ā Task B ā|ā Task C ā| ā
ā 0 100 200 300ms ā
ā ā
ā PARALLEL (all at once) ā
ā āāāāāāāāāāāāāāāāāāāāāāāā ā
ā |ā Task A ā| ā
ā |ā Task B ā| ā
ā |ā Task C ā| ā
ā 0 100ms ā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Loops and Iteration
For...of Loop (Sequential)
async function processSequentially(items) {
const results = [];
for (const item of items) {
const result = await processItem(item);
results.push(result);
}
return results;
}
forEach Does NOT Work
// ā WRONG - forEach doesn't wait for async callbacks
async function broken(items) {
items.forEach(async (item) => {
await processItem(item); // These run in parallel!
});
// Function returns immediately, not when processing is done
}
// ā
Use for...of instead
async function correct(items) {
for (const item of items) {
await processItem(item);
}
}
Parallel Processing with map
async function processParallel(items) {
const promises = items.map((item) => processItem(item));
return await Promise.all(promises);
}
Controlled Concurrency
async function processBatched(items, batchSize) {
const results = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const batchResults = await Promise.all(
batch.map((item) => processItem(item))
);
results.push(...batchResults);
}
return results;
}
Common Patterns
Retry Pattern
async function retry(fn, attempts = 3, delay = 1000) {
for (let i = 0; i < attempts; i++) {
try {
return await fn();
} catch (error) {
if (i === attempts - 1) throw error;
await new Promise((r) => setTimeout(r, delay));
console.log(`Retry ${i + 1}/${attempts - 1}`);
}
}
}
// Usage
const data = await retry(() => fetchData(url), 3, 1000);
Timeout Pattern
async function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Timeout')), ms);
});
return Promise.race([promise, timeout]);
}
// Usage
try {
const data = await withTimeout(fetchData(), 5000);
} catch (error) {
if (error.message === 'Timeout') {
console.log('Request timed out');
}
}
Polling Pattern
async function poll(fn, interval, maxAttempts) {
for (let i = 0; i < maxAttempts; i++) {
const result = await fn();
if (result.ready) {
return result;
}
await new Promise((r) => setTimeout(r, interval));
}
throw new Error('Polling timeout');
}
// Usage
const status = await poll(() => checkJobStatus(jobId), 1000, 30);
Debounce Async
function debounceAsync(fn, delay) {
let timeoutId;
let pendingPromise = null;
return async function (...args) {
clearTimeout(timeoutId);
return new Promise((resolve) => {
timeoutId = setTimeout(async () => {
const result = await fn.apply(this, args);
resolve(result);
}, delay);
});
};
}
const debouncedSearch = debounceAsync(searchAPI, 300);
Top-Level Await
In ES Modules (ES2022)
// In .mjs file or type: "module" in package.json
const config = await fetchConfig();
const db = await connectDatabase(config);
export { db };
In Classic Scripts (Use IIFE)
// Without top-level await, use IIFE
(async () => {
const data = await fetchData();
console.log(data);
})();
Async Generators
Creating Async Generators
async function* asyncGenerator() {
yield await fetch('/api/1');
yield await fetch('/api/2');
yield await fetch('/api/3');
}
// Consuming
for await (const response of asyncGenerator()) {
console.log(await response.json());
}
Practical Example: Paginated API
async function* fetchPages(url) {
let nextUrl = url;
while (nextUrl) {
const response = await fetch(nextUrl);
const data = await response.json();
yield data.items;
nextUrl = data.nextPage;
}
}
// Usage
for await (const page of fetchPages('/api/items')) {
console.log('Got page:', page);
}
Best Practices
1. Always Handle Errors
// ā
Good
async function fetchData() {
try {
return await apiCall();
} catch (error) {
logger.error(error);
throw error;
}
}
// ā Bad - unhandled rejections
async function fetchData() {
return await apiCall();
}
2. Avoid Unnecessary await
// ā Unnecessary - adding extra microtask
async function getValue() {
return await Promise.resolve(42);
}
// ā
Better - return directly
async function getValue() {
return Promise.resolve(42);
}
3. Use Promise.all for Independent Operations
// ā Slow - sequential
async function getData() {
const users = await getUsers();
const posts = await getPosts();
return { users, posts };
}
// ā
Fast - parallel
async function getData() {
const [users, posts] = await Promise.all([getUsers(), getPosts()]);
return { users, posts };
}
4. Be Careful with this
class Service {
constructor() {
this.value = 42;
}
// ā
Method retains 'this'
async getValue() {
return this.value;
}
}
const service = new Service();
const fn = service.getValue.bind(service); // Bind if passing around
Summary
| Concept | Description |
|---|---|
async | Declares function returns Promise |
await | Pauses until Promise resolves |
| Error handling | Use try/catch blocks |
| Sequential | await one after another |
| Parallel | Start all, await Promise.all |
| for...of | Sequential async loop |
| forEach | Does NOT work with async |
| Top-level await | ES modules only |
| Async generators | async function* with for await |
What's Next?
In the next section, we'll dive deeper into Error Handling patterns for async code, including:
- ā¢Centralized error handling
- ā¢Error boundaries
- ā¢Retry strategies
- ā¢Graceful degradation