The async Function
Prefix any function declaration, expression, or arrow function with async to make it asynchronous. An async function always returns a Promise β if you return a plain value, it is automatically wrapped in Promise.resolve().
// All three are equivalent
async function greet(name) {
return `Hello, ${name}!`;
}
const greetArrow = async (name) => `Hello, ${name}!`;
const obj = {
async greet(name) { return `Hello, ${name}!`; }
};
// All return Promises
const result = greet('Alice');
console.log(result instanceof Promise); // true
result.then(msg => console.log(msg)); // Hello, Alice!
// Return value is auto-wrapped
async function add(a, b) {
return a + b; // returns Promise.resolve(a + b)
}
add(3, 4).then(sum => console.log(sum)); // 7
true Hello, Alice! 7
The await Expression
await can only be used inside an async function. It pauses the function's execution until the awaited Promise resolves, then returns the resolved value. Other code outside the async function continues to run while it is paused.
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function makeBreakfast() {
console.log('Starting breakfast...');
console.log('Boiling water...');
await delay(500); // pause 500ms
console.log('Water boiled!');
console.log('Toasting bread...');
await delay(300);
console.log('Toast ready!');
console.log('Brewing coffee...');
await delay(400);
console.log('Coffee ready!');
return 'Breakfast is served!';
}
makeBreakfast().then(msg => console.log(msg));
console.log('(Outside async function β runs immediately)');
Starting breakfast... Boiling water... (Outside async function β runs immediately) Water boiled! Toasting bread... Toast ready! Brewing coffee... Coffee ready! Breakfast is served!
Error Handling with try/catch/finally
Use standard try/catch with async/await β the syntax works exactly as with synchronous code. A rejected Promise becomes a thrown error that the catch block receives.
async function fetchUser(id) {
if (id <= 0) throw new RangeError('ID must be positive');
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
if (!response.ok) throw new Error(`HTTP error: ${response.status}`);
return response.json();
}
async function displayUser(id) {
let spinner;
try {
spinner = 'β³ Loading...';
console.log(spinner);
const user = await fetchUser(id);
console.log(`Name: ${user.name}`);
console.log(`Email: ${user.email}`);
return user;
} catch (err) {
// Handles BOTH thrown errors and rejected Promises
console.error(`Error: ${err.message}`);
return null;
} finally {
spinner = null; // cleanup always runs
console.log('Loading complete (success or failure)');
}
}
displayUser(1); // success path
displayUser(-1); // error path (throws RangeError)
β³ Loading... β³ Loading... Error: ID must be positive Loading complete (success or failure) Name: Leanne Graham Email: Sincere@april.biz Loading complete (success or failure)
Sequential vs Parallel await
A common mistake is awaiting Promises sequentially when they are independent β this wastes time. Use Promise.all() to run independent async operations in parallel.
function fetchData(label, ms) {
return new Promise(resolve =>
setTimeout(() => resolve(`${label} data`), ms)
);
}
// β Sequential: total ~900ms (each waits for the previous)
async function sequential() {
const start = Date.now();
const a = await fetchData('User', 300);
const b = await fetchData('Posts', 400);
const c = await fetchData('Friends', 200);
console.log(`Sequential: ${Date.now() - start}ms`, [a, b, c]);
}
// β
Parallel: total ~400ms (the longest one)
async function parallel() {
const start = Date.now();
const [a, b, c] = await Promise.all([
fetchData('User', 300),
fetchData('Posts', 400),
fetchData('Friends', 200)
]);
console.log(`Parallel: ${Date.now() - start}ms`, [a, b, c]);
}
sequential();
parallel();
Parallel: ~400ms ['User data', 'Posts data', 'Friends data'] Sequential: ~900ms ['User data', 'Posts data', 'Friends data']
Only parallelise operations that are truly independent. If step B requires the result of step A (e.g., get userId then fetch user data), sequential awaits are correct. Forcing parallel execution on dependent operations will cause errors.
async/await vs .then() β Same Power, Better Syntax
| Aspect | .then() chains | async/await |
|---|---|---|
| Readability | Can become nested/long | Reads like synchronous code |
| Error handling | .catch() at end of chain | try/catch (familiar) |
| Debugging | Stack traces can be confusing | Clear stack traces |
| Conditional logic | Awkward with if/else | Natural if/else, loops |
| Return values | Explicit return in each .then() | Use return normally |
| Under the hood | Promises | Promises (syntactic sugar) |
Top-Level await (ES2022 Modules)
In ES modules (type="module" in browsers or .mjs files in Node.js), you can use await at the top level of a file β without wrapping in an async function.
// In an ES module (type="module" or .mjs file)
// Top-level await β valid without async wrapper
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
const todo = await response.json();
console.log(todo.title); // "delectus aut autem"
// Useful for dynamic imports
const { default: lodash } = await import('lodash');
// Useful for conditional configuration
const config = process.env.NODE_ENV === 'test'
? await import('./config.test.mjs')
: await import('./config.prod.mjs');
When top-level await is unavailable (CommonJS, older environments), wrap your code in an immediately-invoked async function: (async () => { ... })(). This is a common pattern in Node.js scripts.
Async Class Methods
class UserService {
#baseUrl = 'https://jsonplaceholder.typicode.com';
async getUser(id) {
const res = await fetch(`${this.#baseUrl}/users/${id}`);
if (!res.ok) throw new Error(`User ${id} not found`);
return res.json();
}
async getPosts(userId) {
const res = await fetch(`${this.#baseUrl}/posts?userId=${userId}`);
return res.json();
}
async getUserWithPosts(userId) {
// Parallel fetch β both requests fire at the same time
const [user, posts] = await Promise.all([
this.getUser(userId),
this.getPosts(userId)
]);
return { ...user, posts };
}
}
const svc = new UserService();
svc.getUserWithPosts(1)
.then(data => {
console.log(`${data.name} has ${data.posts.length} posts`);
})
.catch(console.error);
Leanne Graham has 10 posts
ποΈ Practical Exercise
Build a DataLoader class that:
- Has an async
load(urls)method that fetches multiple URLs in parallel withPromise.all. - Has an async
loadSequential(urls)method using afor...ofloop. - Both methods return an array of parsed JSON objects.
- Wrap each fetch in try/catch so a single failure doesn't abort the whole batch β return
nullfor failed items. - Add a private
#cacheMap to avoid re-fetching the same URL.
π₯ Challenge Exercise
Implement an asyncQueue function that processes an array of items through an async worker function with a configurable concurrency limit, collects all results in order (even if faster tasks finish first), and reports progress as tasks complete. No external libraries allowed β only async/await and Promises.
Interview Questions
- What does an async function always return?
- Can you use await outside an async function? (What about top-level await?)
- How does try/catch work with async/await?
- What is the performance problem with sequential awaits on independent Promises?
- How does async/await differ from .then() at the engine level?
- What is an async IIFE and when would you use one?
π Summary
asyncfunctions always return Promises; plain return values are auto-wrapped.awaitpauses the async function until the Promise resolves, returning its value.- Use
try/catch/finallyfor error handling β rejected Promises become throw errors. - Use
Promise.all()to run independent async operations in parallel, not sequential awaits. - Top-level await is available in ES modules (ES2022).
- async/await is syntactic sugar over Promises β same power, much more readable.
Frequently Asked Questions
No. await only pauses the async function that contains it. The JavaScript event loop is free to run other code while the awaited Promise is pending. This is fundamentally different from blocking synchronous code.
Technically yes β you can pass an async function to forEach β but forEach does not await the returned Promise, so all iterations run concurrently and errors are silently ignored. Use a for...of loop for sequential async iteration, or Promise.all(arr.map(async ...)) for parallel.
For global handling, wrap the entire function body in one try/catch. For per-operation handling, either wrap each await in its own try/catch, or use the pattern const [err, data] = await somePromise.then(d => [null, d]).catch(e => [e, null]) β a Go-style error-first return.