JavaScript Async/Await Tutorial
Master async and await in JavaScript — write asynchronous code that reads like synchronous code, handle errors with try/catch, and parallelize work with Promise.all.
What you'll learn
- ✓What async functions actually return
- ✓How await pauses execution without blocking
- ✓Error handling with try/catch
- ✓Running tasks in parallel correctly
- ✓Common async pitfalls to avoid
Prerequisites
- •Familiarity with JavaScript basics
async/await is syntactic sugar over promises, but it transforms how you think about asynchronous code. Instead of chaining then callbacks, you write code that looks linear and synchronous. In this tutorial we will walk through the mechanics and the patterns that matter most.
What async Means
The async keyword on a function does two things. First, it guarantees the function returns a promise. Second, it allows you to use await inside the function body. Even if you return a plain value, it will be wrapped in a resolved promise automatically.
async function getNumber() {
return 42;
}
getNumber().then(n => console.log(n)); // 42
This means callers always handle the result with then or another await.
How await Works
await pauses execution of the async function until the awaited promise settles. The key insight is that this pause does not block the main thread — the rest of your program keeps running. Under the hood, the function is suspended and resumed when the promise resolves.
async function loadUser(id) {
const res = await fetch(`/api/users/${id}`);
const user = await res.json();
return user;
}
Compare this to the equivalent chained version and you will immediately see why developers prefer it for readable flow control.
Error Handling with try/catch
One of the biggest wins of async/await is unified error handling with normal try/catch blocks. Any rejection becomes a thrown error, so you can use familiar synchronous patterns.
async function safeLoad(id) {
try {
const user = await loadUser(id);
return user;
} catch (err) {
console.error('Failed to load user', err);
return null;
}
}
You can also use finally for cleanup, just like with synchronous code.
Sequential vs Parallel
A common mistake is awaiting promises sequentially when they could run in parallel. Each await waits for the previous one, so independent calls become slow.
// Slow: 2 seconds total if each takes 1
const a = await fetchA();
const b = await fetchB();
// Fast: 1 second total
const [a2, b2] = await Promise.all([fetchA(), fetchB()]);
Always start independent promises first, then await them together. This single habit can speed up an app dramatically.
Top-Level Await
Modern JavaScript modules support top-level await. You can use it directly at the top of an ES module without wrapping the code in an async function.
// In an ES module
const config = await fetch('/config.json').then(r => r.json());
export default config;
This is great for module initialization, but be aware that it delays the module’s loading, which cascades to dependents.
Loops and async
Using await inside a for loop processes items one at a time. Using Array.prototype.forEach with an async callback does not wait at all — forEach ignores the returned promises. Choose carefully.
// One at a time
for (const id of ids) {
await processOne(id);
}
// All at once
await Promise.all(ids.map(id => processOne(id)));
When you need controlled concurrency (say, ten at a time), reach for a small helper like p-limit or write a queue.
Pitfalls to Avoid
Three traps catch newcomers. First, returning a promise from inside a try without awaiting it means the catch will never see its rejection. Always return await if you need the catch to fire.
Second, async functions in array methods like filter or map return promises, not booleans or values. You usually need Promise.all afterwards.
Third, do not forget that async always returns a promise. If a caller forgets to await, errors become unhandled rejections that may crash your Node process.
Wrapping Up
Async/await turns asynchronous control flow into ordinary-looking code while preserving all the power of promises. Combine it with Promise.all for parallelism and try/catch for clean error handling, and your async code will be both fast and maintainable.
Related articles
- JavaScript The JavaScript Event Loop, Explained Clearly
A deep but practical look at the JavaScript event loop: call stack, microtasks, macrotasks, and how async code actually runs in the browser and Node.
- JavaScript JavaScript Generators and Iterators
A practical guide to JavaScript iterators and generator functions: the protocols, lazy sequences, async generators, and where they shine in real code.
- JavaScript JavaScript Promises Deep Dive
Go beyond the basics of JavaScript promises — explore the microtask queue, error propagation, cancellation patterns, and advanced combinators like any and allSettled.
- JavaScript async/await in JavaScript: A Practical Guide
A practical guide to async functions and await — writing asynchronous code that reads like synchronous code, handling errors with try/catch, and running work in parallel.