JavaScript async/await Patterns You Should Know
Asynchronous programming is a core part of modern JavaScript, and async/await has made it much more readable. But many developers still struggle with common pitfalls: unhandled promise rejections, slow sequential requests, or confusing error handling. In this article, we'll explore practical async/await patterns that you can apply immediately to write cleaner, more efficient code.
1. Basic async/await syntax
An async function always returns a Promise. The await keyword pauses execution until the Promise settles, then returns its resolved value. If the Promise rejects, it throws an error that you can catch with try/catch.
async function fetchData() {
try {
const response = await fetch('/api/data');
const data = await response.json();
return data;
} catch (error) {
console.error('Fetch failed:', error);
}
}
This is the foundation. But real-world scenarios often require more nuanced approaches.
2. Handling errors gracefully
One of the biggest advantages of async/await is that you can use try/catch for both synchronous and asynchronous errors. However, be careful with multiple await calls in a single try block: if the first one fails, subsequent ones won't execute.
For better granularity, you can wrap each await in its own try/catch or use a helper function to avoid repetitive try/catch blocks.
async function getUser(id) {
try {
const user = await fetchUser(id);
return user;
} catch (error) {
console.error('Failed to fetch user:', error);
throw error;
}
}
3. Running promises in parallel
Awaiting multiple independent promises sequentially is a common performance mistake. If you need results from multiple sources, use Promise.all() to run them concurrently.
// Sequential: slow
const user = await fetchUser();
const posts = await fetchPosts();
// Parallel: fast
const [user, posts] = await Promise.all([fetchUser(), fetchPosts()]);
Use Promise.allSettled() when you want to wait for all promises to settle, regardless of whether they resolve or reject. This is useful when you want to collect results even if some fail.
4. Sequential execution when order matters
Sometimes you need to process items in order, such as when each step depends on the previous one. In that case, use a for...of loop with await instead of forEach (which doesn't wait for async callbacks).
for (const item of items) {
await processItem(item);
}
If you need to limit concurrency, consider libraries like p-limit or implement a simple queue.
5. Avoiding the async/await pitfall in loops
Using await inside Array.prototype.forEach is a classic mistake: forEach doesn't await the async callback, so the loop finishes before the promises resolve. Always use for...of or map with Promise.all.
6. Async/await with timeouts
To prevent hanging requests, you can race a promise against a timeout.
function timeout(ms) {
return new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms)
);
}
await Promise.race([fetchData(), timeout(5000)]);
This pattern is handy for API calls that might hang.
7. Async generators and for await...of
Async generators allow you to produce a stream of values asynchronously. You can consume them with for await...of.
async function* fetchPages() {
let page = 1;
while (true) {
const data = await fetch(`/api/items?page=${page}`);
if (data.length === 0) break;
yield data;
page++;
}
}
for await (const page of fetchPages()) {
console.log(page);
}
This is great for paginated APIs or streaming data.
8. Common mistakes and how to avoid them
- Forgetting to await: Calling an async function without await returns a Promise, and errors may go unhandled.
- Using await in non-async functions: Syntax error. Always mark the function as async.
- Overusing sequential awaits: Use Promise.all when possible.
- Not handling rejections: Always add .catch or try/catch.
9. Best practices for clean async code
- Prefer async/await over raw Promises for readability.
- Use Promise.all for independent operations.
- Handle errors at the appropriate level.
- Keep async functions small and focused.
- Use async/await with caution in loops; choose the right pattern.
FAQ
What is the difference between async/await and Promises?
Async/await is syntactic sugar over Promises, making asynchronous code look synchronous. It simplifies chaining and error handling with try/catch.
Can I use await inside a forEach loop?
No, forEach does not wait for async callbacks. Use for...of or Promise.all with map instead.
How do I run multiple async operations in parallel?
Use Promise.all() to run them concurrently and wait for all to resolve.
When working with JSON APIs, you can quickly format and validate responses using our JSON Formatter.