@cafiyndotcom A 4th one worth adding: errors nobody sees.
5-minute test: trigger a deliberate error in production and check it actually reaches you, with a readable stack trace (not minified line 1) and an alert someone reads. A quiet dashboard can just mean broken reporting.
@Ogbuaborchimao1 On the unhandled rejections one: since Node 15 they crash the process by default, so a fire-and-forget call without await or .catch() can take a server down.
Handle the error where the promise is created. Keep process.on('unhandledRejection') as a last-resort logger, then exit.
Cancel a fetch with AbortController and it rejects with an AbortError (a DOMException). AbortSignal.timeout() gives a TimeoutError.
Report every rejected fetch and users leaving a page become "errors".
Check the error name first: report timeouts, skip deliberate aborts.
try/catch around items.forEach(async item => ...) catches nothing.
forEach doesn't wait for the promises it creates, so the try block ends first and each rejection becomes an unhandledRejection.
Use for...of with await, or await Promise.all(https://t.co/rYLxa0fTyN(fn)).
Promise.any() failed and the log says "All promises were rejected"?
It's an AggregateError. The real reasons sit in its errors array:
catch (err) {
if (err instanceof AggregateError)
err.errors.forEach(e => console.error(e))
}
Log those, or every failure looks alike.
@HoenyefiaGideon@oscar_edem99095 A tiny guard for exactly this: bake the commit SHA into the build and show it at /version or in the footer. Most hosts expose it as an env var (on Vercel it's VERCEL_GIT_COMMIT_SHA). Then "what's actually live?" takes five seconds instead of an afternoon.
@DarshanD3v And if it should show the visitor's own timezone, the server can't know it. Render a placeholder and format the date after mount, or put suppressHydrationWarning on just that one <time> element (it only applies one level deep, so it won't hide other mismatches).
try/catch only covers code that runs while the try block is running.
try {
setTimeout(() => { throw new Error('boom') }, 0)
} catch (e) {
// never runs
}
The callback throws later. In Node that's an uncaughtException. Put the try/catch inside the callback.
In an async function, this catch never runs when fetchUser rejects:
try {
return fetchUser(id)
} catch (e) {
log(e)
}
The promise leaves the try block before it settles.
Write return await inside try whenever you want the catch (or finally) to apply.
fetch() has no timeout option. Node's fetch waits up to 5 minutes for response headers, so a hung upstream hangs your request too.
Set your own limit:
const res = await fetch(url, {
signal: AbortSignal.timeout(5000),
})
It rejects with a TimeoutError you can catch and log.
JSON.stringify(new Error("db timeout")) returns "{}".
message and stack are non-enumerable, so a logger that JSON-serialises the error ships an empty object to your prod logs.
Pull the fields out yourself:
const { name, message, stack } = err
log({ name, message, stack })
@sebastienlorber The part SPA folks will feel most: INP and CLS used to pile up over the whole session and get pinned on the landing URL.
Now each soft nav is its own view, and the CrUX API reports it under the URL users navigated to. A slow /settings can't hide behind / anymore.
@_jaydeepkarale One Node-specific check before calling it a leak: compare process.memoryUsage().heapUsed with rss.
heapUsed climbing = JS objects being retained, so diff two heap snapshots.
rss climbing while heap stays flat = Buffers/native memory. Look at external and arrayBuffers instead.
Since Node 15, an unhandled promise rejection crashes the process by default.
One fire-and-forget call like sendWelcomeEmail(user), with no await and no .catch(), can take a server down when the email API times out.
Await it, or add a .catch() that logs the error.
@veerbal01 "Failed without an error" is the scary one. A check that catches it: emit a success event at the end of the work (webhook_processed, email_sent) and alert when it stops arriving. A 200 that skipped the work shows up as silence.
(We build Telemetry Tracker, which does this.)
@k_ritij24 The cheapest fix we know: generate a request id, put it on the one log line with route + status + stack, and return it in the error response. A user screenshot then maps to a log line.
And listen for unhandledrejection. A lot of "no logs" errors are promises nobody awaited.
New in Telemetry Tracker: alerts for events that didn't happen.
Send trackEvent('signup_verified'), add a "No events (heartbeat)" rule with a 1โ24h window, and we email you when it goes quiet.
Checked about every 5 min. No recovery notice yet.
https://t.co/I2HvEuutnw
Next.js gotcha: redirect() works by throwing.
try {
await save(data)
redirect('/done')
} catch (err) {
console.error(err) // swallows the redirect
}
Move redirect() after the try/catch, or rethrow it with unstable_rethrow(err) at the top of your catch.
fetch() doesn't throw on a 404 or 500.
It only rejects on network failures. So this "works" while your API is down:
const res = await fetch(url)
const data = await res.json()
Check res.ok and throw (with status + route) so the failure actually reaches your error tracker.
@somehowali Good reminder. Two quick checks while you're in those logs:
1. curl your own /.env and /.git/config from outside. They should 404, not download.
2. If you run error tracking, filter bot 404s on random paths so the noise doesn't bury real alerts.