What Happens When You Type a URL and Press Enter
DNS lookups, TCP and TLS handshakes, HTTP requests, servers, and the browser's rendering pipeline. The whole journey of a web page, explained so it finally clicks.
Read →Missing awaits, await inside forEach, accidental waterfalls and swallowed errors. The async/await bugs that pass code review and fail in production, with the fix for each one.
async/await made asynchronous JavaScript readable. It also made asynchronous bugs look like normal code, which is exactly why they slip through review. These are the mistakes that show up again and again in real codebases, from beginner projects to large production apps.
awaitasync function saveUser(user) {
db.users.insert(user) // no await
return { ok: true }
}
The function returns "ok" before the insert finishes. If the insert fails, nobody hears about it. Worse, the bug is intermittent, because it depends on timing.
Fix: await every promise you depend on. Turn on the @typescript-eslint/no-floating-promises rule (or require-await style rules in plain JS). It catches this automatically.
await inside forEachids.forEach(async (id) => {
await sendEmail(id)
})
console.log('All emails sent') // they aren't
forEach ignores the promises your callback returns. The log runs immediately, and errors become unhandled rejections.
Fix: use for...of for sequential work, or Promise.all with map for parallel work:
for (const id of ids) {
await sendEmail(id) // one at a time
}
await Promise.all(ids.map(id => sendEmail(id))) // all at once
const user = await getUser(id)
const orders = await getOrders(id)
const settings = await getSettings(id)
Three independent requests, run one after another. If each takes 200ms, you've spent 600ms when you could have spent 200.
Fix: start them together.
const [user, orders, settings] = await Promise.all([
getUser(id),
getOrders(id),
getSettings(id),
])
Only keep things sequential when one call genuinely needs the result of another.
Promise.all when you wanted partial successPromise.all rejects as soon as any promise rejects. If you're loading five dashboard widgets and one API is down, you lose all five.
Fix: Promise.allSettled gives you every result, success or failure:
const results = await Promise.allSettled(widgets.map(loadWidget))
const loaded = results.filter(r => r.status === 'fulfilled').map(r => r.value)
The flip side of mistake 3: Promise.all over 10,000 items fires 10,000 requests at once. You'll hit rate limits, exhaust database connections or get your IP blocked.
Fix: process in batches, or use a small concurrency limiter such as p-limit:
import pLimit from 'p-limit'
const limit = pLimit(5)
await Promise.all(items.map(item => limit(() => processItem(item))))
try/catch that doesn't catchtry {
return fetchData() // returned, not awaited
} catch (err) {
// never runs for async failures
}
Without await, the promise rejects after the try block has already exited. Use return await inside try blocks.
try {
await chargeCard(order)
} catch (e) {
console.log(e)
}
The payment failed, the log scrolled past, and the code carries on as if everything were fine. Catch errors only where you can do something useful: retry, show a message, fall back. Otherwise let them propagate to a handler that logs and reports them properly.
response.ok with fetchfetch only rejects on network failure. A 404 or 500 resolves normally.
const res = await fetch('/api/user')
if (!res.ok) throw new Error(`Request failed: ${res.status}`)
const data = await res.json()
This one causes a lot of Cannot read properties of undefined errors three functions later.
A hung request can wait a very long time. Give important calls a deadline with AbortController:
const res = await fetch(url, { signal: AbortSignal.timeout(8000) })
The user types "re", then "rea", then "react". Three search requests fly out, and the slowest one (for "re") arrives last and overwrites the correct results.
Fix: cancel the previous request when a new one starts, or ignore responses that are no longer current. In React, the effect cleanup function is the natural place for that (more on effects here).
process.on('unhandledRejection', ...)) and log them loudly.When something does break, a systematic approach beats guesswork: how to debug code systematically.
No. It's syntax on top of promises. Performance depends on how you structure the calls, especially whether independent work runs in parallel.
Use Promise.all for independent tasks that can run in parallel (with a limit for large lists). Use for...of when order matters or each step depends on the previous one.
Every async function returns a promise. You need await (or .then) where you call it to get the resolved value.
DNS lookups, TCP and TLS handshakes, HTTP requests, servers, and the browser's rendering pipeline. The whole journey of a web page, explained so it finally clicks.
Read →There are dozens of HTTP status codes, but about fifteen cover nearly everything. What each one means, when to return it, and the mix-ups (401 vs 403, 301 vs 302) that confuse everyone.
Read →A, AAAA, CNAME, MX, TXT and NS records in plain English, why DNS changes seem slow, and the troubleshooting commands that tell you what is really going on.
Read →