JavaScript Async/Await — Stop Writing Callback Hell
Advertisement
Introduction
Why This Matters
Asynchronous programming is at the heart of JavaScript. Every API call, database query, file read, and timer is async. The evolution from callbacks to Promises to async/await made async code progressively more readable and maintainable. In 2026, async/await is the universal standard — used in React data fetching, Node.js API handlers, Next.js server components, and every modern JavaScript codebase.
Understanding async deeply also means understanding error propagation, concurrent execution with Promise.all, cancellation with AbortController, and how async functions interact with event loops. These are the patterns that separate production-quality code from tutorials.
The Problem: Callback Hell
// Old way — deeply nested callbacks
getUser(userId, (err, user) => {
if (err) return handleError(err)
getPosts(user.id, (err, posts) => {
if (err) return handleError(err)
getComments(posts[0].id, (err, comments) => {
if (err) return handleError(err)
console.log(comments)
})
})
})Promises
// Promise chaining — better, but still verbose
getUser(userId)
.then(user => getPosts(user.id))
.then(posts => getComments(posts[0].id))
.then(comments => console.log(comments))
.catch(err => handleError(err))
.finally(() => setLoading(false))async/await — Clean and Readable
async function loadUserComments(userId) {
try {
const user = await getUser(userId)
const posts = await getPosts(user.id)
const comments = await getComments(posts[0].id)
return comments
} catch (err) {
handleError(err)
} finally {
setLoading(false)
}
}Fetching Data with async/await
interface Post {
id: number
title: string
body: string
userId: number
}
async function fetchPost(id: number): Promise<Post> {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`)
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`)
}
return response.json() as Promise<Post>
}
// Usage
const post = await fetchPost(1)
console.log(post.title)Running Promises Concurrently
// Sequential — slow (requests run one after another)
const user = await fetchUser(1) // wait
const posts = await fetchPosts(1) // wait
// Concurrent — fast (requests run in parallel)
const [user, posts] = await Promise.all([
fetchUser(1),
fetchPosts(1),
])
// Race — use whichever resolves first
const result = await Promise.race([
fetchFromPrimary(),
fetchFromFallback(),
])
// allSettled — wait for all, don't fail on partial errors
const results = await Promise.allSettled([fetchA(), fetchB(), fetchC()])
results.forEach(r => {
if (r.status === "fulfilled") console.log(r.value)
else console.error(r.reason)
})
// any — resolve when first succeeds (reject only if all fail)
const fastest = await Promise.any([fetchA(), fetchB(), fetchC()])Error Handling Patterns
// Per-call try/catch
async function getUserSafe(id: number) {
try {
return await fetchUser(id)
} catch {
return null // fallback
}
}
// Result pattern (avoids try/catch nesting)
async function safeAsync<T>(
promise: Promise<T>
): Promise<[T, null] | [null, Error]> {
try {
const data = await promise
return [data, null]
} catch (err) {
return [null, err as Error]
}
}
const [user, err] = await safeAsync(fetchUser(1))
if (err) {
console.error("Failed:", err.message)
} else {
console.log(user.name)
}AbortController — Cancellation
async function fetchWithTimeout(url: string, timeoutMs: number) {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), timeoutMs)
try {
const response = await fetch(url, { signal: controller.signal })
return await response.json()
} catch (err) {
if (err instanceof DOMException && err.name === "AbortError") {
throw new Error("Request timed out")
}
throw err
} finally {
clearTimeout(timeout)
}
}
// React example: cancel on unmount
useEffect(() => {
const controller = new AbortController()
fetch("/api/data", { signal: controller.signal })
.then(res => res.json())
.then(setData)
.catch(err => {
if (err.name !== "AbortError") setError(err)
})
return () => controller.abort()
}, [])Async Iteration
async function* paginate(url: string) {
let page = 1
while (true) {
const res = await fetch(`${url}?page=${page}`)
const data = await res.json()
if (!data.length) break
yield data
page++
}
}
for await (const page of paginate("/api/users")) {
console.log(`Fetched ${page.length} users`)
}Common Mistakes
- Using
awaitinsideforEach— it doesn't work; usefor...oforPromise.all - Not handling the rejection of
asyncfunctions — unhandled promise rejections crash Node.js - Running sequential awaits when they could be concurrent with
Promise.all - Forgetting that
asyncfunctions always return a Promise, even when returning a plain value - Mixing
.then()andawaitin the same function — pick one style
Best Practices
- Always
awaitor.catch()every Promise — never let rejections go unhandled - Use
Promise.allfor independent concurrent operations to reduce total wait time - Use
AbortControllerin ReactuseEffectto cancel pending fetches on unmount - Prefer
Promise.allSettledwhen partial failures are acceptable - Write a
safeAsyncwrapper for clean error handling without deeply nested try/catch
Key Takeaways
async/awaitis syntactic sugar over Promises —asyncfunctions always return a Promiseawaitpauses execution until the Promise resolves; errors propagate to the nearesttry/catchPromise.allruns Promises concurrently — failure of any rejects the whole callPromise.allSettledwaits for all, regardless of individual failureAbortController+signalis the standard way to cancel fetch requestsfor await...ofiterates async generators — use it for paginated API responses- Never use
awaitinsideforEach— usefor...oforPromise.all(arr.map(...))
Advertisement
Related reading
TypeScript for Backend Developers — Complete 2024 Guide5 min readTypeScript vs JavaScript - Which Should You Use in 2026?5 min readWeb Security Best Practices Every Developer Must Know5 min readData Corruption from Bad Serialization — When Your Data Silently Changes6 min readESM vs CommonJS in 2026 — The Definitive Guide to Node.js Module Interop7 min readNode.js 22 Features Every Backend Engineer Must Know6 min read