async Function Basics
Any function can be made async. It automatically wraps the return value in a Promise.
// Regular function vs async function
function getAge() { return 25; } // returns 25
async function getAge2() { return 25; } // returns Promise<25>
// All these are equivalent - async works with any function syntax
async function named() { return 'a'; }
const arrowAsync = async () => 'b';
const exprAsync = async function() { return 'c'; };
// The return value is always a Promise
const result = getAge2();
console.log(result instanceof Promise); // true
result.then(val => console.log(val)); // 25
// async functions with class methods
class UserService {
async getUser(id) {
const response = await fetch('/api/users/' + id);
return response.json();
}
}
// Calling an async function - it returns a Promise
const user = await new UserService().getUser(1);
// Or with .then()
new UserService().getUser(1).then(user => console.log(user));
The await Keyword
await pauses the async function and waits for the Promise to settle. The event loop continues running other tasks during the pause.
async function loadUserProfile(userId) {
// await pauses here until fetch() Promise resolves
const response = await fetch('/api/users/' + userId);
// response is now the resolved value (a Response object)
const user = await response.json(); // await again for the JSON parse
console.log(user.name);
return user; // async functions always return a Promise
}
// Sequential - each await waits for the previous
async function loadSequential() {
const user = await fetchUser(1); // waits ~100ms
const profile = await fetchProfile(user.id); // then waits ~100ms
const posts = await fetchPosts(user.id); // then waits ~100ms
// Total: ~300ms - slow because they run one after the other
return { user, profile, posts };
}
// await works with any Promise, not just fetch
async function example() {
await new Promise(resolve => setTimeout(resolve, 1000)); // wait 1s
const data = await Promise.resolve({ key: 'value' }); // instantly
console.log(data.key); // 'value'
}
// await on a non-Promise just returns the value (harmless)
const x = await 42; // x === 42
Error Handling
Rejected Promises become thrown errors when awaited - catch them with a standard try/catch block.
// try/catch - catches both network errors and thrown errors inside
async function loadUser(id) {
try {
const response = await fetch('/api/users/' + id);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return await response.json();
} catch (err) {
console.error('Failed to load user:', err.message);
return null; // return a default instead of propagating the error
}
}
// finally always runs
async function fetchWithCleanup(url) {
showSpinner();
try {
const res = await fetch(url);
return await res.json();
} catch (err) {
logError(err);
throw err; // re-throw to let caller handle it too
} finally {
hideSpinner(); // runs whether success or failure
}
}
// Granular error handling - different catch per step
async function processOrder(orderId) {
let order;
try {
order = await fetchOrder(orderId);
} catch {
throw new Error('Order not found');
}
try {
await chargePayment(order.total);
} catch (err) {
await cancelOrder(orderId);
throw new Error('Payment failed: ' + err.message);
}
return sendConfirmation(order);
}
// .catch() shorthand for one-liners
const user = await fetchUser(1).catch(() => null);
Parallel Execution
Start multiple async operations without awaiting them immediately, then await all at once with Promise.all().
// SLOW: sequential - each waits for the previous
async function slowDashboard() {
const user = await fetchUser(); // 200ms
const posts = await fetchPosts(); // 200ms
const comments = await fetchComments();// 200ms
// Total: ~600ms
}
// FAST: parallel - all three start simultaneously
async function fastDashboard() {
const [user, posts, comments] = await Promise.all([
fetchUser(), // starts immediately
fetchPosts(), // starts immediately
fetchComments(), // starts immediately
]);
// Total: ~200ms (longest single request)
render(user, posts, comments);
}
// Start manually without await, then collect
async function manualParallel() {
const userPromise = fetchUser(); // starts now
const postsPromise = fetchPosts(); // starts now
// Do some synchronous work...
const config = loadConfig();
// Now collect the async results
const user = await userPromise;
const posts = await postsPromise;
return { user, posts, config };
}
// allSettled - parallel but handles individual failures
async function robustLoad(ids) {
const results = await Promise.allSettled(ids.map(id => fetchUser(id)));
return results
.filter(r => r.status === 'fulfilled')
.map(r => r.value);
}
A for loop with await runs each iteration after the previous one finishes - they are sequential, not parallel. To process an array of items in parallel, use await Promise.all(arr.map(async item => ...)). Only use the sequential loop when order matters or you need to limit concurrency.
Async Iteration Patterns
// Sequential: for...of with await (order preserved, one at a time)
async function processInOrder(items) {
for (const item of items) {
await process(item); // each waits for previous
}
}
// Parallel: map + Promise.all (all at once)
async function processInParallel(items) {
await Promise.all(items.map(item => process(item)));
}
// Controlled concurrency (e.g., max 3 at a time)
async function processWithLimit(items, limit = 3) {
const results = [];
for (let i = 0; i < items.length; i += limit) {
const batch = items.slice(i, i + limit);
const batchResults = await Promise.all(batch.map(process));
results.push(...batchResults);
}
return results;
}
// async for...of with an async iterable (streams, paginated APIs)
async function* paginate(url) {
let nextUrl = url;
while (nextUrl) {
const res = await fetch(nextUrl);
const data = await res.json();
yield data.items;
nextUrl = data.nextPage ?? null;
}
}
for await (const items of paginate('/api/products')) {
renderBatch(items);
}
Top-Level await
In ES modules (type="module" or .mjs files), you can use await at the top level without wrapping in an async function.
// In an ES module (type="module" script or .mjs file)
// Load config before the rest of the module executes
const config = await fetch('/api/config').then(r => r.json());
console.log('Config loaded:', config.version);
// Initialize database connection
const db = await connectToDatabase(config.dbUrl);
// Conditional import
const { default: polyfill } = await import('./polyfill.js');
// Export values that depend on async initialization
export { db, config };
// Modules that import this file wait for the top-level awaits to resolve
// import { db } from './db.mjs'; // this line waits for db to be ready
// NOTE: top-level await is NOT available in classic scripts (no type="module")
// and in CommonJS (require()). It pauses the entire module until resolved.
Common Pitfalls
// Pitfall 1: await in forEach (doesn't wait)
async function bad() {
const ids = [1, 2, 3];
ids.forEach(async id => {
await processId(id); // forEach doesn't await these!
});
console.log('done'); // prints before processing finishes
}
// Fix: use for...of
async function good() {
for (const id of [1, 2, 3]) {
await processId(id);
}
console.log('done'); // prints after all processing
}
// Pitfall 2: not awaiting an async function - silent bug
async function saveUser(user) { /* ... */ }
async function createAndSave() {
const user = buildUser();
saveUser(user); // forgot await! errors are silently swallowed
console.log('saved'); // prints immediately even if saveUser fails
}
// Fix:
async function createAndSave2() {
const user = buildUser();
await saveUser(user);
console.log('saved');
}
// Pitfall 3: mixing async/await and .then() chains - hard to read
async function mixed() {
const data = await fetch('/api/data')
.then(r => r.json()) // mixing await + .then
.then(d => d.filter(Boolean)); // confusing!
return data;
}
// Better: consistent style
async function consistent() {
const res = await fetch('/api/data');
const json = await res.json();
return json.filter(Boolean);
}