Home JavaScript Unhandled Promise Rejection Crashing Node? Fix It
Intermediate 7 min · September 23, 2026

Unhandled Promise Rejection Crashing Node? Fix It

Stop unhandled promise rejections by awaiting with try/catch, adding .catch(), and wrapping Express routes the right way..

N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Drawn from code that ran under real load.

Follow
Production
production tested
September 23, 2026
last updated
1,905
articles · all by Naren
Before you start⏱ 15 min
  • Comfort with async/await and promises
  • Basic Express routing knowledge
  • A Node 18+ API project to audit
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Every promise needs an owner: await it inside try/catch or end the chain with .catch() so failures can't go unhandled
  • Modern Node crashes on unhandled rejections by default, so one missed catch can restart your whole process
  • Use process.on('unhandledRejection') only to log and locate the source, never as a substitute for real handling
  • Wrap async Express routes so rejected promises reach next(err) instead of hanging the request
✦ Definition~90s read
What is Unhandled Promise Rejection Fix?

A promise rejection becomes unhandled when a promise settles to rejected state with no .catch(), no second argument to .then(), and no surrounding try/catch on its await. Node tracks this per microtask checkpoint: if a rejected promise has no handler attached by the end of the tick, it emits unhandledRejection on the process object.

Imagine ordering food delivery and then leaving the house with your phone off.

If that event has no listener, Node's default behavior since version 15 is to throw — printing the rejection reason and exiting with a nonzero code. The --unhandled-rejections=warn flag restores the old log-and-continue behavior, but that only converts crashes into silent data corruption.

The reason crashes are the default is that continuing is worse. An unhandled rejection means some async operation failed and nobody decided what that means: a payment charged without an order record, a cache write skipped while the response claimed success, a socket left open while its owner moved on.

Resuming execution after such ambiguity produced the horror stories that motivated the default change. The crash is Node telling you the program state can't be trusted.

In web servers the dominant source is async route handlers. Express 4 doesn't await returned promises, so an async handler that throws rejects into the void: the request hangs until timeout while the rejection kills the process seconds later. The fix family has three members: await everything inside try/catch at the leaves, terminate floating chains with .catch(), and wrap framework boundaries so rejections flow into error middleware via next(err).

A process-level unhandledRejection listener belongs in every service, but strictly as a locator that logs the stack and exits — never as a recovery mechanism.

Plain-English First

Imagine ordering food delivery and then leaving the house with your phone off. The driver arrives, can't reach you, and the food sits on the doorstep getting cold. An unhandled rejection is the same: your code started an async job, left no failure instructions, and Node holds a failed delivery with nowhere to send it. Modern Node shuts down the whole service over it. The fix is simple — always leave a contact number by handling every async failure.

Your API runs fine for weeks, then restarts at 3 AM with a single log line: UnhandledPromiseRejection. No request context. No user ID. Just a rejected promise nobody claimed, and Node's default response — crash the process. PM2 restarts it, traffic resumes, and everyone hopes it won't happen again. It will, because the missing handler is still in the code.

This error is uniquely frustrating because the crash site tells you nothing about the bug site. The rejection originates in one async function, floats through the event loop unobserved, and kills the process far from where the mistake lives. Developers add a process-level handler, see the crashes stop getting logged, and declare victory — while requests keep hanging and data keeps half-writing.

This guide shows the full fix. You'll learn why modern Node crashes by default, how to find the unclaimed promise with one handler used strictly as a locator, and the wrapper patterns that route async errors into Express middleware. Real incident, real commands, permanent fix.

Why Missing .catch and try/await Crashes Everything

Every promise needs exactly one owner for its failure path. For awaited promises the owner is the surrounding try/catch. For chained promises it's a terminal .catch() or a rejection handler passed as .then's second argument. Miss both and the rejection floats: no code observes it, no fallback runs, and at the microtask checkpoint Node flags it unhandled. A single floating promise in a helper called once per hour is enough to crash a service that otherwise handles millions of requests.

The treacherous variants are the ones that look handled. Returning a promise from a non-async callback without chaining leaves it floating. A .then() with only a success handler ignores rejections. Promise.all rejects wholesale on the first failure, discarding the rest. Audit async code by tracing each promise to its owner — if you can't name the catch, you've found the crash. TypeScript's no-floating-promises rule automates exactly this audit.

Timer and event callbacks hide the most floaters. setTimeout(async () => {...}) without an inner try/catch floats its rejection past every outer handler, and EventEmitter listeners that return promises (instead of handling within) do the same on every emit. Array.map with async callbacks is another trap: the mapped promises only stay owned if you await Promise.all on the result — forEach with async callbacks floats every single one, always. Promise.race and Promise.any add subtlety: losers that later reject still need handlers, or they surface as unhandled long after the race settled. Enable the no-floating-promises rule and the no-misused-promises rule together; the pair catches detached chains, unawaited async arguments, and conditional promises assigned but never returned. You'll turn a whole category of invisible crashes into red squiggles in the editor.

src/payments.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
async function chargeOrder(order) {
  try {
    const charge = await stripe.charges.create({
      amount: order.total,
      currency: 'usd',
    }, { idempotencyKey: order.id });
    await db.orders.record(order.id, charge.id);
    return charge;
  } catch (err) {
    logger.error('charge failed', { orderId: order.id, code: err.code });
    throw err;
  }
}

// Chained style always terminates:
// createAudit(entry).catch((err) => logger.error('audit failed', err));
Try it live
📊 Production Insight
A nightly report job floated one promise per customer — 4,000 unhandled rejections by morning. The process crashed at 6 AM daily for a week before a no-floating-promises lint rule named every one.
🎯 Key Takeaway
Trace each promise to its catch. If you can't name the owner of a rejection, the process will eventually meet it.

Crash by Default: Why Modern Node Refuses to Continue

Before Node 15, unhandled rejections printed a warning and kept running. Services survived — with silently skipped writes, half-updated caches, and phantom successes. The default changed because the working group concluded an ambiguous program state is more dangerous than downtime. A crashed container gets replaced by a healthy one in seconds; a surviving-but-corrupt process serves wrong answers indefinitely.

Don't fight this with --unhandled-rejections=warn. The flag converts a diagnosable crash into invisible corruption and teaches the team to ignore rejection warnings. Instead, design for crash-only recovery: keep handlers stateless, persist in-flight work in a queue, and let the orchestrator restart fast. Your process-level listener should log the rejection with full context and exit nonzero. Treat every unhandled rejection as a bug report, not a runtime condition to tolerate.

Crash-only recovery shapes how you design state. Keep request handlers stateless so any process can serve any retry; push in-flight work into durable queues (BullMQ, SQS) before acknowledging it; store session data in Redis rather than process memory so a restart loses nothing visible. With that architecture, a crash costs one orchestrator restart — seconds — instead of corrupted state that poisons hours of traffic. Tune restart policy deliberately: RestartAlways with backoff in Kubernetes, plus crash-loop alerts that fire when restarts exceed three in ten minutes (a crash loop signals a deterministic bug, not a blip). Test the path with chaos drills: inject a rejection in staging and verify traffic recovers without manual steps. Teams that rehearse crash recovery stop fearing the crash log and start treating it as the bug report it is.

src/bootstrap.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
process.on('unhandledRejection', (reason, promise) => {
  logger.fatal('unhandled rejection — exiting', {
    reason: reason?.stack ?? String(reason),
    at: new Date().toISOString(),
  });
  setTimeout(() => process.exit(1), 500).unref();
});

process.on('uncaughtException', (err) => {
  logger.fatal('uncaught exception — exiting', { stack: err.stack });
  process.exit(1);
});
Try it live
⚠ Never Swallow Rejections to Stay Alive
A handler that logs and continues leaves the process in unknown state. Log with context, flush telemetry, and exit. Orchestrators restart clean processes in seconds.
📊 Production Insight
A team ran six months on warn mode. When they finally audited, 2% of orders had no audit record — 3,100 ghost orders. Crash-by-default would have surfaced the bug on day one.
🎯 Key Takeaway
Crash-only design beats warn-and-continue. Log the rejection, exit fast, and let the platform restart you clean.

The Locator Handler: Finding the Source in Minutes

A process-level unhandledRejection listener is a diagnostic instrument, not a fix. Its job is capturing the rejection reason, the promise object, and ideally the async context (request ID, user ID) before exiting. Run staging with --trace-warnings to attach creation stacks, and ship request-scoped context via AsyncLocalStorage so the locator log names the route and user. Without context propagation, the log shows a bare StripeConnectionError and you're grep-hunting.

Build the locator once and reuse it: serialize reason stacks, include memory and uptime for crash-loop detection, and flush logs synchronously before exit. In Kubernetes, a nonzero exit triggers a restart with the log already shipped — the replacement serves traffic while you read the stack. Delete any handler that resolves or ignores; if code review can't tell locator from suppressor, name the function logAndExitOnUnhandled.

AsyncLocalStorage is what makes locator logs worth reading. Create one async context per incoming request carrying request ID, route, user ID, and start time; the unhandledRejection listener then reads the current store and attaches it to the crash log. Without this, concurrent requests blur into one anonymous stack — with it, the log names the exact request that spawned the floater. Propagate the same context into outbound calls and queue jobs so the trail survives service boundaries. Keep the store small (IDs and timestamps, not bodies) to avoid memory overhead in hot paths. Add a unit test that triggers a rejection inside a stubbed context and asserts the locator output contains the request ID — untested instrumentation rots, and a silent locator is worse than none because the team assumes coverage it doesn't have.

BASH
1
2
3
node --trace-warnings --unhandled-rejections=strict server.js 2>&1 | head -30
grep -rn "unhandledRejection" src/ | head
node -e "process.on('unhandledRejection', r => { console.error('LOCATOR:', r?.stack); process.exit(1); }); Promise.reject(new Error('boom-x'))"
📊 Production Insight
Adding AsyncLocalStorage request IDs to the locator turned 40-minute crash hunts into 3-minute reads: each rejection log named its route, user, and payload size.
🎯 Key Takeaway
Use the process handler strictly to locate and exit. Context-rich logs plus a fast exit beat every suppression trick.

Async Route Wrappers: The Express Pattern That Ends Hangs

Express 4 invokes your handler and ignores a returned promise. An async route that throws rejects into the void: no error middleware runs, the client hangs until timeout, and Node crashes separately on the unhandled rejection. Two failures for the price of one missing wrapper. Every async handler must bridge its rejection into next(err) explicitly.

The standard wrapper is five lines: a higher-order function catching the promise and forwarding to next. Apply it at route definition so no handler can forget, or adopt express-async-errors to patch the router globally. Express 5 handles async rejections natively, so upgrading removes the category — but until then, the wrapper is mandatory. Audit with a grep for bare async handlers and convert each one; partial coverage still crashes.

Middleware chains need the same treatment as routes. Async auth, validation, and logging middleware that throw will hang requests identically to bare routes, so wrap app.use handlers with the same asyncWrap helper — or better, wrap at registration through a tiny router shim that every file imports instead of raw express.Router. Audit coverage mechanically: grep for 'async (req' and 'async(req' across routes and middleware, and fail CI when an unwrapped match appears outside the wrapper. For WebSocket and queue-consumer boundaries, apply the identical pattern (catch plus dead-letter) since those frameworks also ignore returned promises. Upgrading to Express 5 removes the route-level need but not the discipline — background handlers in any framework still need owners. One shared wrapper module plus a lint gate ends the category across every boundary your service owns.

src/routes.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const asyncWrap = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

app.post('/webhooks/stripe', asyncWrap(async (req, res) => {
  const event = await verifyWebhook(req);
  await handleEvent(event);
  res.json({ received: true });
}));

app.use((err, req, res, next) => {
  logger.error('request failed', { path: req.path, msg: err.message });
  res.status(502).json({ error: 'upstream failed, retry later' });
});
Try it live
📊 Production Insight
Wrapping 63 async routes in one codemod ended a client's weekly 3 AM crash loop. Request hangs dropped from 400 per week to zero, and error middleware finally saw every failure.
🎯 Key Takeaway
Express 4 drops async rejections — wrap every handler to forward into next(err) or upgrade to Express 5.

Fire-and-Forget Promises: Queues Instead of Floats

Background work — audit logs, analytics pings, cache warmups — tempts developers to call the async function and move on. That floating promise has no owner; its rejection crashes the process from a code path nobody associates with the request. The fix isn't awaiting it (that would slow the response) but giving it an owner that isn't the request: a .catch() that logs, or better, a durable queue.

For truly optional side effects, attach .catch with structured logging so failures are visible without being fatal. For anything that must eventually succeed — order records, webhook acknowledgments, billing events — enqueue in BullMQ or SQS and acknowledge only after persistence. Queues convert unhandled rejections into retried jobs with backoff and dead-letter visibility. If losing the side effect is acceptable, .catch suffices; if it isn't, it was never fire-and-forget.

Queue configuration decides whether the safety net holds under real failure. Set explicit attempts (5 for billing-critical, 3 for best-effort), exponential backoff with jitter to avoid thundering herds on recovery, and a dead-letter queue with alerts so permanently failing jobs page a human instead of retrying forever. Make jobs idempotent — keyed by event ID with dedupe on the consumer — because at-least-once delivery replays jobs after crashes, and a non-idempotent audit writer double-counts exactly when you're investigating an incident. Monitor queue depth and age: growing depth means consumers are down (scale or fix), while aging head items mean poison messages blocking the queue (dead-letter them). For optional pings, a bare .catch with a debug log suffices — but revisit any .catch that stays silent for months, since quiet failures compound into the data gaps you'll discover during audits.

src/audit.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
const { Queue } = require('bullmq');

const auditQueue = new Queue('audit', { connection: { host: 'redis' } });

async function auditAsync(entry) {
  await auditQueue.add('write', entry, {
    attempts: 5,
    backoff: { type: 'exponential', delay: 2000 },
  });
}

// Optional pings: owned but non-blocking
// analytics.ping(user).catch((err) => logger.warn('ping failed', err));
Try it live
📊 Production Insight
Moving audit writes from floating promises to a BullMQ queue cut unhandled rejections to zero and survived a 4-hour analytics outage with automatic catch-up on recovery.
🎯 Key Takeaway
Background work needs an owner: .catch for optional pings, a real queue for anything that must succeed.

Retrying Right: Backoff, Timeouts, and Idempotency

Most rejections worth handling come from the network: timeouts, resets, 5xx responses. Blind retries amplify outages; structured retries with exponential backoff, jitter, and AbortSignal timeouts absorb them. Cap attempts (3-5 for user-facing paths), bound total elapsed time under the client's patience, and always send idempotency keys on mutating calls so a retried charge returns the original instead of duplicating it.

Separate retryable errors (ETIMEDOUT, ECONNRESET, 503) from terminal ones (400, 401, validation failures) and retry only the former. Log each attempt with attempt number and elapsed time so dashboards show retry storms forming. Combine with circuit breakers when a dependency stays down: fail fast with 502 instead of stacking doomed attempts. Handled rejections with this discipline turn 3 AM pages into boring metrics.

Circuit breakers complete the retry story. After consecutive failures pass a threshold (say 50% over 30 seconds), the breaker opens and calls fail fast with 502 instead of consuming threads on a known-dead dependency — preserving your pool for recovery instead of stacking doomed attempts. Half-open probes (one trial request per interval) detect recovery without a retry storm, closing the breaker when success returns. Combine breakers with hedged requests for latency-sensitive reads: send the same GET to two endpoints after the 95th-percentile wait, and take whichever answers first — you trade a little extra load for dramatically lower tail latency. Log breaker state transitions as events, not metrics, so incident timelines show exactly when the dependency was declared dead and when it recovered. Retries handle blips; breakers handle outages; hedging handles slowness — deploy all three and 3 AM stays quiet.

BASH
1
2
node -e "fetch('https://httpbin.org/delay/10', { signal: AbortSignal.timeout(3000) }).catch(e => console.error('timed out as designed:', e.name))"
grep -rn 'idempotencyKey\|Idempotency-Key' src/ | head
📊 Production Insight
Adding idempotency keys plus 3-attempt backoff turned a Stripe brownout from 214 duplicate charges into 1,900 clean retries with zero duplicates.
🎯 Key Takeaway
Retry only transient failures with backoff, timeouts, and idempotency keys. Terminal errors should fail fast with clear status codes.
● Production incidentPOST-MORTEMseverity: high

One Missing Catch Double-Charged 214 Customers at 3 AM

Symptom
At 3:04 AM the payments API restarted 6 times in 11 minutes. Each restart logged UnhandledPromiseRejection: StripeConnectionError with no request ID. Between restarts, Stripe retried unacknowledged webhooks, and because the charge succeeded before the database write that never ran, every retry created a second charge. By 3:15 AM, 214 customers had duplicate charges totaling $31,400. The crash loop masked the double-charge pattern: each restart looked like a transient network blip, and the on-call engineer kept waiting for it to settle.
Assumption
The team assumed Stripe was having an outage and their retries were safe. They added a process.on('unhandledRejection') handler that logged the error and kept the process alive, which stopped the restarts — and guaranteed every subsequent webhook failure silently skipped the order write while returning nothing to Stripe. Retries continued, duplicates kept growing, and the log spam hid the pattern for another 40 minutes. The handler-as-fix turned a loud crash into quiet data corruption.
Root cause
The webhook handler was declared async but awaited the Stripe charge without try/catch, and the Express 4 route passed the bare async function with no wrapper. When Stripe's API timed out after the charge committed server-side, the promise rejected, Express never saw it, the response hung, and Node crashed on the unhandled rejection. The database write placing the order record sat after the charge in the same try-less block, so it never executed. No idempotency key was sent to Stripe, so each retried webhook created a fresh charge instead of returning the original.
Fix
Four changes shipped within 24 hours. Every async route was wrapped so rejections route to Express error middleware with next(err). The charge-then-record sequence gained Stripe idempotency keys derived from the webhook event ID, making retries safe. Await blocks around external calls got try/catch with explicit 502 responses so Stripe receives actionable feedback. The process-level handler was kept but rewritten to log-and-exit for fast container replacement. All 214 duplicate charges were refunded ($31,400) within 48 hours, and webhook replays since show zero duplicates across 90,000 events.
Key lesson
  • Never let a process-level rejection handler keep serving traffic. Log, capture context, and exit — a fresh process with known state beats a surviving one with unknown corruption.
  • Send idempotency keys on every mutating external call. Retries are guaranteed in webhook systems; only idempotency makes them safe to receive twice.
  • Wrap every async framework boundary. Express 4 won't forward rejected promises, so one unwrapped route can hang requests and crash the process simultaneously.
Production debug guideFive techniques that take you from a bare crash log to the exact unclaimed promise — in order of speed.5 entries
Symptom · 01
Logs show UnhandledPromiseRejection with no request context or route name
Fix
Add a locator listener temporarily: run node --trace-warnings --unhandled-rejections=strict server.js to get the rejection stack, and attach process.on('unhandledRejection', (r, p) => { console.error('UNHANDLED at', p, r?.stack); process.exit(1); }). The stack points at the originating await — fix that call site, then keep the exit-on-unhandled behavior permanently.
Symptom · 02
Requests hang until timeout and the process crashes seconds later
Fix
Suspect a bare async Express route: run grep -rn 'async (req' src/routes/ to list unwrapped handlers, then confirm with curl -m 10 http://localhost:3000/suspect-route observing the hang. Wrap each handler so rejections call next(err): app.get('/x', (req, res, next) => handler(req, res).catch(next)).
Symptom · 03
Rejection mentions StripeConnectionError, ECONNRESET, or fetch failures
Fix
Isolate the external call with node -e "fetch('https://api.stripe.com/v1/charges', {signal: AbortSignal.timeout(5000)}).catch(e => console.error(e.cause ?? e))" to reproduce the failure mode. Then wrap the call in try/catch with retries plus idempotency keys, returning 502 to callers instead of hanging.
Symptom · 04
Crashes cluster around deploys or dependency upgrades
Fix
Diff behavior with npm ls <suspect> and node --unhandled-rejections=strict ./smoke.js running your critical async paths. New library versions often turn resolved-with-error-values into true rejections. Pin the dependency, add .catch() coverage, and gate upgrades on the smoke script.
Symptom · 05
Floating promises inside loops or event emitters with no owner
Fix
Hunt them with grep -rn '\.then(' src | grep -v catch and grep -rn 'async' src/handlers/ checking each call site awaits or returns the promise. Convert fire-and-forget calls to void tracked with .catch(log) or a queue, and lint with no-floating-promises to prevent recurrence.
Unhandled Rejection — Causes Compared
Root CauseHow to ConfirmFixPrevention
Missing try/catch on awaitStrict-mode stack names the await lineWrap in try/catch with fallbackno-floating-promises lint rule
Bare async Express 4 routeRequest hangs then process crashesasyncWrap forwarding to next(err)Codemod all routes; consider Express 5
Floating background promisegrep finds .then without catchAttach .catch or move to queueLint plus BullMQ for durable work
Suppressor process handlerHandler logs but never exitsRewrite to log-and-exitReview gate on process handlers
Retried mutating call, no keyDuplicates after webhook retriesAdd idempotency keys plus backoffKey-required wrapper for payments
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
srcpayments.jsasync function chargeOrder(order) {Why Missing .catch and try/await Crashes Everything
srcbootstrap.jsprocess.on('unhandledRejection', (reason, promise) => {Crash by Default
node --trace-warnings --unhandled-rejections=strict server.js 2>&1 | head -30The Locator Handler
srcroutes.jsconst asyncWrap = (fn) => (req, res, next) => {Async Route Wrappers
srcaudit.jsconst { Queue } = require('bullmq');Fire-and-Forget Promises
node -e "fetch('https://httpbin.org/delay/10', { signal: AbortSignal.timeout(300...Retrying Right

Key takeaways

1
Own every rejection
try/catch on awaits, terminal .catch on chains.
2
Crash-by-default protects you; warn mode hides corruption.
3
Process handlers locate and exit
they never substitute for handling.
4
Wrap all async Express routes into next(err) or upgrade to Express 5.
5
Queue durable background work; float nothing critical.
6
Retry transient failures with backoff, timeouts, and idempotency keys.

Common mistakes to avoid

6 patterns
×

Using the process handler as the fix instead of a locator

Symptom
Crashes stop but data corruption grows: skipped writes, phantom successes, hanging requests.
Fix
Rewrite the handler to log context and exit nonzero; fix each rejection at its source.
×

Leaving Express 4 async routes unwrapped

Symptom
Requests hang to timeout while the rejection crashes the process separately.
Fix
Wrap every async handler to forward rejections to next(err), or upgrade to Express 5.
×

Floating fire-and-forget promises for critical writes

Symptom
Order records vanish during dependency brownouts with no error surfaced to callers.
Fix
Enqueue durable work in BullMQ/SQS; reserve bare .catch for truly optional pings.
×

Running Node with --unhandled-rejections=warn

Symptom
Warning spam nobody reads while 2% of records silently skip persistence.
Fix
Restore strict crash-by-default and treat each crash log as a bug report.
×

Retrying payments without idempotency keys

Symptom
Every webhook retry after a timeout creates a fresh duplicate charge.
Fix
Derive idempotency keys from event IDs and send them on all mutating calls.
×

Catching errors without logging the request context

Symptom
Catch blocks hide which route, user, and payload triggered the failure.
Fix
Propagate request IDs via AsyncLocalStorage and log them in every catch and locator.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What exactly makes a promise rejection unhandled?
Q02SENIOR
Why does Node crash on unhandled rejections instead of continuing?
Q03SENIOR
An Express 4 async route throws. What happens to the request and the pro...
Q04SENIOR
Is process.on('unhandledRejection') a valid error-handling strategy?
Q05SENIOR
Design retry logic for a payment charge that times out. What prevents do...
Q01 of 05JUNIOR

What exactly makes a promise rejection unhandled?

ANSWER
A rejected promise with no .catch, no second .then argument, and no enclosing try/catch on its await by the microtask checkpoint. I'd explain Node emits unhandledRejection and crashes by default since v15.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Should I use .catch() or try/catch?
02
Does Promise.all need special handling?
03
How do I find which promise went unhandled?
04
Is express-async-errors safe for production?
05
What belongs in a fire-and-forget .catch()?
06
How many retries are reasonable?
N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Drawn from code that ran under real load.

Follow
Verified
production tested
September 23, 2026
last updated
1,905
articles · all by Naren
🔥

That's Node.js. Mark it forged?

7 min read · try the examples if you haven't

Previous
Node ENOENT No Such File Fix
25 / 30 · Node.js
Next
Node ETIMEDOUT Network Fix