Unawaited Promises Cause 40% Data Loss in Node.js
40% of payment webhooks lost data: an unawaited async write returned 200 but never persisted.
20+ years shipping production JavaScript and front-end systems at scale. Drawn from code that ran under real load.
- ✓Solid grasp of fundamentals
- ✓Comfortable reading code examples
- ✓Basic production concepts
- Node.js delivers errors through four separate channels: throw, callbacks, Promise rejections, and EventEmitter 'error' events — missing any one channel means silent failures
- A Promise rejection without .catch() crashes the process in Node.js 15+ (and still does in Node.js 22 LTS)
- Custom error classes with isOperational flag let your global handler distinguish bugs from expected failures — without this flag, you either crash on every validation error or keep running after a genuine bug
- Express async route handlers silently swallow errors unless wrapped with an asyncHandler utility — this is the most common Express production bug in 2026
- Promise.allSettled lets partial results succeed when one operation fails — use it for dashboards and independent operations, not all-or-nothing flows
- The most dangerous bug: calling an async function without await inside try/catch — the catch block never fires, no error surfaces, and data is silently lost
- Node.js 22 LTS ships with a stable built-in diagnostics channel API that makes structured error observability significantly easier without third-party APM agents
Imagine you're a chef running a restaurant kitchen. When an order comes in for a dish you're out of ingredients for, you don't just silently throw the ticket in the bin — you tell the waiter, who tells the customer, who can then order something else. Node.js error handling is exactly that chain of communication. Without it, your app quietly fails while your users stare at a spinning loader, wondering what happened. The tricky part is that in Node.js, there are four completely separate ways an error can happen — and you need to intercept every one of them. Miss even one, and failures disappear without a trace. Good error handling is how your app says 'something went wrong, and here's what' instead of dying in silence.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
Node.js powers millions of production servers, APIs, and real-time applications — and the difference between an app that recovers gracefully from failure and one that crashes at 2 AM taking your database connection pool with it comes down almost entirely to error handling. It is not a nice-to-have. It is the foundation of reliable software, and it is also the area where experienced engineers make the most expensive mistakes.
The core problem Node.js introduces is that errors can arrive from multiple timelines simultaneously. A file read, a database query, an outbound HTTP call, and a timer can all fail at different moments, and if you are not deliberately intercepting each failure path, Node.js will eventually throw an uncaught exception and terminate your process. Worse, with Promises, errors can fail completely silently unless you have wired up rejection handlers — no crash, no log, no alert, just missing data.
In 2026, Node.js 22 LTS is the active long-term support release. The error handling model has not fundamentally changed since Node.js 15 introduced the throwing unhandledRejection default, but Node.js 22 brings a stable diagnostics channel API that makes structured error observability meaningfully easier, and the --experimental-require-module flag is now stable, which affects how error handling patterns compose across ESM and CommonJS boundaries in mixed codebases.
By the end of this article you will understand the four error delivery mechanisms in Node.js, how to build a layered error handling strategy using custom error classes, when to use try/catch versus .catch(), how to handle process-level uncaught exceptions safely, and how to structure middleware-based error handling in an Express API. Every pattern here is grounded in real production behaviour, not toy examples.
Why Unhandled Promise Rejections Corrupt Data
Node.js error handling is the discipline of catching and responding to synchronous and asynchronous failures before they cascade into data loss or crashes. The core mechanic: every thrown exception or rejected promise must be caught by a try/catch, a .catch() handler, or an event listener like process.on('unhandledRejection'). Without it, Node.js terminates the process on uncaught exceptions, and unhandled rejections silently swallow errors — leaving your database in an inconsistent state.
In practice, async error propagation differs from sync: a rejected promise that lacks a .catch() doesn't throw immediately — it waits for garbage collection, then triggers an 'unhandledRejection' event. This delay fools developers into thinking their code is safe. Meanwhile, the error is lost, and downstream operations continue with corrupted data. The key property: unhandled rejections do not crash the process by default (unlike uncaught exceptions), making them silent data corrupters.
Use explicit error boundaries at every async boundary — route handlers, event emitters, and stream pipelines. In production, this means wrapping every async function in try/catch and attaching .catch() to every promise chain. The cost of omission: a single unawaited promise in a payment processing pipeline can double-charge customers or leave orders in limbo. Real systems must treat unhandled rejections as fatal — log them, alert on them, and crash the process to force a clean restart.
The Four Ways Node.js Delivers Errors — and Why Missing One Means Silent Failure
In a browser, errors mostly arrive from one or two places: synchronous throws and maybe a fetch() rejection. Node.js is different in a way that surprises engineers who come from frontend backgrounds. Because Node.js is designed for I/O-heavy work — reading files, querying databases, making HTTP requests, managing socket connections — errors arrive through four completely separate delivery channels. Miss any one of them and you have silent failures that only surface under production load, or worse, never surface at all.
The first channel is synchronous throws. These work exactly as you would expect — a try/catch block handles them reliably. The second is the error-first callback pattern, the original Node.js convention established in the early fs and http core modules, where the first argument to every callback is always an Error object or null. The third is Promise rejections, which arrived with modern async APIs and require either .catch() chaining or try/catch inside async functions. The fourth is EventEmitter error events, used by streams, HTTP servers, database connections, and most core network modules — if you do not attach an 'error' event listener and the emitter fires one, Node.js throws an uncaught exception by default, even in Node.js 22.
Understanding which channel a library uses is the first question you should ask before integrating anything new into your codebase. The Node.js core fs module uses error-first callbacks in its synchronous-style async API and Promises in the fs/promises variant. The native fetch() API (stable in Node.js 21+, available in Node.js 22) uses Promise rejections. An HTTP or TCP server uses EventEmitter. Most modern ORMs use Promises. Get the channel wrong and you have error handling code that looks completely correct, passes code review, and catches nothing.
- Synchronous throw: caught by try/catch — the simplest and most predictable. JSON.parse, URL constructor, and synchronous validators all use this channel.
- Error-first callback: check the first argument — if truthy, it is an Error. Always return immediately after handling, or execution falls through to the success path with undefined data.
- Promise rejection: caught by .catch() or await + try/catch. In Node.js 22, unhandled rejections exit the process with code 1 by default — there is no grace period.
- EventEmitter 'error': attach .on('error', handler) before calling any methods. No handler means uncaught exception means process crash — unchanged in Node.js 22.
- diagnostics_channel (Node.js 22 stable): subscribe to error events across third-party libraries without monkey-patching. Useful for APM integration and structured logging pipelines.
- First question when integrating any library: which channel does it use? The answer determines what your error handling code needs to look like.
fetch(), fs/promises, most modern ORMsCustom Error Classes — Stop Throwing Plain Strings and Start Throwing Structured Data
When you throw new Error('something failed') everywhere, your error handlers have almost nothing to work with. Is this a validation error the user caused? A database timeout you should retry? A third-party API going down? A configuration problem that will never resolve? You cannot tell — and neither can your monitoring tools, your on-call engineer at 3 AM, or the automated system that decides whether to restart the process.
The solution is custom error classes that extend the built-in Error. This gives every error a type you can check with instanceof, a machine-readable errorCode property you can switch on programmatically, an HTTP statusCode if you are building an API, an isOperational flag that tells your crash handler whether to keep running or exit, and any additional context — the offending field name, the failing query, the downstream service that timed out — that makes debugging faster. This is how Express, Mongoose, Prisma, and virtually every mature Node.js library handles errors internally.
The key insight is that errors are first-class data structures, not just messages. They carry information about what went wrong, who caused it, whether it is safe to retry, and how to respond to the client. A generic Error discards all of that context the moment it is created. A custom error class preserves it all the way from the database layer up through the service layer and into the HTTP response layer, so your global error handler can send a 422 for validation failures, a 503 for service timeouts, a 429 for rate limit breaches, and a 500 for genuine bugs — without needing a fragile if/else chain that parses error.message strings.
In 2026, with TypeScript being the standard for most serious Node.js codebases, custom error classes also benefit from full type safety — you can define the shape of each error class as an interface, and TypeScript will enforce that your catch blocks handle every error type your service can throw.
Error() in service layer code is a code smell.Express Global Error Middleware — One Handler to Rule Them All
In a real Express API, you could put try/catch blocks in every single route handler and write error response logic inline wherever an error occurs. But that means duplicating your error formatting in dozens of places. When you need to change how errors are structured — switching to RFC 7807 (Problem Details for HTTP APIs), adding a request ID to every error response, or integrating a new observability platform — you are editing dozens of files. That approach does not scale.
Express has a built-in mechanism for centralised error handling: a middleware function with exactly four arguments (err, req, res, next). Express detects the four-argument signature at registration time using function.length and treats it as an error-handling middleware, only invoking it when next(error) is called somewhere upstream. This is the correct pattern: route handlers use try/catch and call next(error) on failure, and one global handler at the bottom of your middleware stack owns all error formatting, logging, and response decisions.
The critical behaviour that trips up almost everyone: Express does not automatically intercept errors thrown inside async route handlers. If an async function throws and nobody calls next(error), Express never sees the error. In Node.js 22, that becomes an unhandled Promise rejection that exits the process. You need the asyncHandler wrapper — a tiny utility that wraps every async route and automatically forwards any rejection to next. This is a one-time infrastructure decision that makes every async route in your application safe.
One change worth noting for 2026: if you are using Express 5 (currently in release candidate and increasingly adopted), Express 5 natively handles Promise rejections from async route handlers — the asyncHandler wrapper is not required. Express 4.x, which remains the dominant version in production, still requires it. Know which version you are on.
Error Correlation IDs — Your Only Hope in a Microservices Blackout
You grep logs across 12 services after a payment failure. Every error is ‘Connection refused’ or ‘Timeout’. Zero context. That’s because you didn’t thread a correlation ID through your async chain. A single UUID per request lets you stitch together a distributed stack trace. Without it, you’re blind in production. Always generate an ID at the HTTP or message boundary. Attach it to every error object. Log it. Pass it downstream via headers or message metadata. When an upstream service crashes, the ID tells you which request caused it. Don’t roll your own UUID generator — use crypto.randomU or a well-tested library. Store the ID in UID()AsyncLocalStorage so it’s available in any function without manual threading. This turns a wall of noise into a searchable slice of time.
Error Wrapping — Preserve the Cause, Not Just the Symptom
Your database throws ECONNREFUSED. Your service catches it and throws DatabaseUnavailableError. Great, but you lose the original stack trace. Now you can't tell if it was a DNS resolution failure or a firewall block. Error wrapping means you nest the original error inside your new one. Node.js has no built-in cause chain, so you manually attach the cause property. When you log the wrapped error, iterate the cause chain to show every layer. This is non-negotiable in a service-oriented architecture. The root cause is rarely at the top of the stack. If you flatten errors, you flatten your ability to diagnose. Every custom error class should accept an optional cause parameter. In your logger, walk error.cause recursively. Print the full chain. That’s how you turn a cryptic ‘operation failed’ into ‘PostgreSQL connection pool exhausted’. Stop masking root causes.
cause property (Node 16.9+) instead of a custom originalError field. Your logger can then traverse it generically.cause property — lose the original stack and you lose the root cause.Graceful Shutdown — Why a SIGTERM Shouldn't Kill Requests Mid-Flight
You deploy a new version. The orchestrator sends SIGTERM. Your process dies instantly. Every in-flight request gets a ECONNRESET or a broken response. Users see 502s. That’s a production incident you caused by ignoring process signals. A graceful shutdown traps SIGTERM and SIGINT, stops the HTTP server, and gives ongoing requests a deadline to finish. Node.js won't do this for you. You must listen for the signal, call , then wait for all pending requests to complete. Use a connection counter or server.close()server.closeIdleConnections(). Set a hard timeout — say 10 seconds — then force-exit. Why? If your database is down, hanging forever wastes resources. The code below is minimal. In production, add draining of message queues and database pools. A process that ignores SIGTERM is a process that corrupts data. Don't be that team.
Async Wrapper — Kill Try/Catch Boilerplate Across Every Route
You've seen the pattern: every async handler wrapped in try/catch, repeating the same next(err) line. That's not engineering — that's manual labor. An async wrapper is a one-liner factory that catches rejected promises and forwards them to Express error middleware. Without it, one missing catch in an async route silently swallows the error. Your logs stay clean, your users get a 500, and you lose the root cause. The wrapper fixes that by intercepting the rejection and calling next(err) automatically. It's a single function you apply at route definition time. No more boilerplate, no more memory leaks from unhandled rejections. Production code demands this pattern because it guarantees every async error has an escape hatch. You write the logic, the wrapper handles the fallthrough.
asyncWrapper in a shared middleware module and import it. Your team will thank you when they add the 50th route.API Response Standardization — Stop Shipping Inconsistent Payloads
Your error handler catches the exception. Great. Now what does the client get? One endpoint returns { error: 'not found' }, another returns { message: 'Invalid ID', code: 400 }. Your frontend team rage-quits because they have to guess the shape. Standardize your API response envelope. Every response — success or failure — should follow the same contract: status, data, error, and a correlation ID. This is not optional in a microservices architecture. Your error middleware should map custom error properties into a uniform JSON structure before sending. That means your controller never calls res.json({ msg }) directly. It throws a structured error, and the global handler serializes it. No exceptions (pun intended). The client always knows where to find the error message and which trace to look for in the logs. Ship a consistent envelope, and your API becomes a black box that behaves predictably.
NODE_ENV is 'development'. Leaking stack traces is a security vulnerability.Using Async-Await — The Unseen Error Paths You're Ignoring
Async-await didn't eliminate error handling; it just shifted where errors hide. When you wrap an async function in a synchronous try/catch, unhandled promise rejections inside that function still bypass your handler. The root cause: async functions return promises, and if you await a rejected promise inside a try block, the catch fires. But if the rejection happens before the await — like in a synchronous exception thrown inside the async function — it becomes an unhandled rejection unless you catch it at the function entry point. Node.js treats these differently: synchronous errors inside async functions propagate to the returned promise's rejection handler, but only if you actually await or chain .catch(). Missing either corrupts your error flow silently. The fix: wrap every async route handler with a higher-order function that catches both sync and async errors. This ensures a thrown Error inside any async function reaches your global error handler, not the abyss of unhandled rejections that Node.js will crash on in future versions.
Quick Checklist — 5 Error Handling Gates Before Production
Most Node.js failures in production trace back to 5 missing gates. First, gate one: every async route handler must be wrapped by a catch-all function. Second, gate two: a global error middleware (4-parameter function) must be registered after all routes. Third, gate three: process.on('unhandledRejection') must log and exit to restart cleanly — never swallow it. Fourth, gate four: all database and external API calls need per-call timeout middleware; hanging promises crash nothing but lock resources. Fifth, gate five: every thrown error must include a unique correlation ID (UUID) for tracing across microservices. Run this checklist: (1) Can you import a route and cause a synchronous throw inside an async function? It must propagate. (2) Does your Express server log the stack trace before sending a 500? (3) When a Redis connection fails mid-request, does the request timeout or return an appropriate 503? (4) Are all custom errors instances of Error? (5) Do you catch errors in Promise.all() — a single rejection rejects the whole group, leaving other promises orphaned. Address these five gates and you've eliminated 90% of silent failures.
process.exit() is brutal but correct — Node.js deprecates unhandled rejections in future versions, and silent corruption is worse than a restart.Explanation — Why Errors Bleed Through the Cracks
Most Node.js developers learn error handling by patching surface symptoms—a try/catch here, an .catch() there—without understanding the four fundamental error delivery mechanisms: synchronous throw, callbacks with err, event emitters emitting 'error', and rejected promises. When you mix these paradigms, errors can vanish silently. A thrown Error inside a setTimeout fires no listener. A promise rejection in an async function that returns a promise? Caught beautifully—but only if you await or chain .catch(). If you fire off an async IIFE without handling its returned promise, that rejection becomes an unhandledPromiseRejection—terminating the process in future Node versions. The silent failures happen precisely where developers assume safety: inside event listeners, streams, or microtask queues. Understanding that every error must eventually hit a handler of its idiom means you stop chasing bugs and start architecting predictable failure paths.
Using Async-Await — The Unseen Error Paths You're Ignoring
Async-await is syntactic sugar over promises, but it introduces a subtle pitfall: errors thrown inside an async function become rejected promises—but only if the function is awaited. Consider an async middleware function in Express: if you call it without await inside a route handler, any rejection becomes an unhandled promise rejection. Worse, when using forEach with an async callback, the forEach method does not await the returned promises—so errors in any iteration are silently swallowed. The fix is simple: use for...of or Promise.allSettled when you need error awareness. Another hidden path: if an async function calls another async function without await, the caller's try/catch sees nothing. Always treat async function calls as promise chains—if you don't await, you must .catch() or the error vanishes into Node's unhandled rejection abyss, crashing your service in Node 15+.
Error.captureStackTrace Edge Cases — When V8 Stack Traces Lie
Error.captureStackTrace is a V8 optimization that strips unnecessary stack frames from Error objects. But it has edge cases that bite you in production. First, if you call it after the Error is constructed, it silently fails — the stack remains unchanged. Second, it only works on Error instances; calling it on plain objects throws a TypeError. Third, when used with custom Error classes, you must pass the constructor as the second argument to exclude it from the stack. If you forget, the constructor frame appears, bloating logs. Fourth, in async contexts, captureStackTrace captures the synchronous call site only — it won't include the async chain. This means your stack trace ends at the first await boundary, losing critical context. To work around this, combine captureStackTrace with async stack traces (see --async-stack-traces flag). Always call captureStackTrace inside the constructor before any other logic, and pass this.constructor as the second argument. Test with both sync and async callers to ensure frames are clean.
this.constructor as second argument, and never after construction. For async errors, supplement with async stack traces.uncaughtExceptionMonitor Hook — Observing Without Swallowing
Node.js 12.7 introduced process.on('uncaughtExceptionMonitor', ...) — a hook that fires before the default uncaught exception handler. Unlike uncaughtException, this monitor does not prevent the process from exiting. It's designed for observability: log the error, send metrics, then let Node terminate. This is critical because uncaughtException is dangerous — if you don't exit, your app may be in an inconsistent state. The monitor gives you a safe way to capture telemetry without risking corruption. Use it to emit structured logs, increment error counters, or alert monitoring systems. But beware: the monitor receives the same error object as the default handler, so if you modify it (e.g., add properties), those changes persist. Also, the monitor is synchronous — don't await promises inside it. For async logging, use process.nextTick or a separate microtask. Finally, the monitor does not catch unhandled promise rejections; for those, use unhandledRejection or the --unhandled-rejections=strict flag.
--async-stack-traces Flag — Recover Lost Async Context
Node.js 12+ supports the --async-stack-traces flag, which preserves stack traces across async boundaries. Without it, an error thrown inside a promise or async function shows only the synchronous call site, not the chain that led there. With the flag, V8 tracks async frames and includes them in the stack. This is a game-changer for debugging: you see where the async operation was initiated, not just where it failed. However, there are trade-offs. First, it has a performance cost — V8 must store additional metadata for each async operation. In high-throughput apps, this can add 5-10% overhead. Second, it only works with native async/await and Promises, not with callbacks or event emitters. Third, the stack trace can become very long, especially in deeply nested async flows. To mitigate, limit stack trace length in logs (e.g., first 20 frames). Enable the flag in development and staging, but consider disabling in production if performance is critical. Alternatively, use it selectively with NODE_OPTIONS='--async-stack-traces' per process.
Error Serialization for Logging — Don't Lose the Stack
When logging errors, JSON.stringify(error) returns {} because Error objects have non-enumerable properties. This silent data loss means you lose stack, name, and custom properties. To serialize properly, use a custom replacer or a library like serialize-error. The replacer must enumerate all own and inherited enumerable properties, plus explicitly include name, message, stack, and cause. For custom error classes, ensure all relevant properties are enumerable or manually added. Also handle circular references — errors can reference themselves via cause chains. A robust serializer recursively walks the error chain, converts each to a plain object, and truncates long stacks. In production, never log the full stack in every log line — it bloats storage. Instead, log a hash of the stack for deduplication, and store full stacks in a separate error store. Use structured logging (e.g., JSON) so your log aggregator can index fields like error.name and error.code. Example: { "error": { "name": "DatabaseError", "message": "timeout", "stack": "...", "query": "SELECT ..." } }.
Cross-Service RFC 7807 Error Propagation — Standardize API Errors
RFC 7807 (Problem Details for HTTP APIs) defines a standard error response format: type, title, status, detail, instance. When propagating errors across microservices, use this format to ensure every service speaks the same error language. In Node.js, create a ProblemError class that extends Error and includes these fields. When an upstream service returns a problem JSON, parse it and re-throw as a ProblemError with the original type and detail. This preserves the error chain and allows the gateway to aggregate errors. For internal service-to-service calls, include a traceId and spanId in the instance field for correlation. The type field should be a URI pointing to documentation. Example: { "type": "https://api.example.com/errors/rate-limit", "title": "Rate Limit Exceeded", "status": 429, "detail": "Too many requests", "instance": "/orders?traceId=abc" }. In Express middleware, catch ProblemError and respond with the same structure. This eliminates guesswork for clients and enables automated error handling.
Error Handling Checklist — 7 Gates Before Production
Before shipping, verify these seven gates to catch silent failures. Gate 1: Every async route is wrapped with an async error handler (e.g., express-async-errors or a wrapper). Gate 2: Global error middleware is registered last and handles all error types (syntax, validation, operational). Gate 3: Unhandled promise rejections are logged and process exits (use --unhandled-rejections=strict). Gate 4: Uncaught exceptions are monitored via uncaughtExceptionMonitor and process exits. Gate 5: All custom errors extend Error and include name, message, stack, and cause. Gate 6: Error serialization is used in logging — never JSON.stringify(error). Gate 7: Graceful shutdown handles SIGTERM/SIGINT: stop accepting requests, drain active connections, then exit. Automate these checks in CI with lint rules (e.g., no unhandled promise rejections) and integration tests that trigger error paths. Use a checklist in your PR template to ensure every developer verifies these gates.
no-unhandled-promise-rejections and test that error middleware returns correct status codes.The Unawaited Promise That Silently Killed 40% of Payment Webhooks
- An unawaited async function is the most dangerous bug class in Node.js — it produces zero errors, zero logs, zero crashes, and zero indication that anything went wrong. The data simply does not arrive.
- ESLint's @typescript-eslint/no-floating-promises rule is not optional in production codebases — it catches exactly this class of bug that code review consistently misses because the code looks syntactically correct.
- Trust but verify: after critical writes, read back the data to confirm it persisted before responding with 200. A 200 response means nothing if the write did not land.
- If your webhook handler returns 200 before confirming the side effect completed, you have a permanent data loss vector that will activate under load.
- Three hours of investigation in the wrong layer is expensive. When data disappears silently, the first question is always: is there an unawaited async call in this code path?
grep -rn 'await\|\.catch(' src/ | grep -v node_modules | grep -v testnode --unhandled-rejections=throw app.js| File | Command / Code | Purpose |
|---|---|---|
| io | function parseWebhookPayload(rawBody) { | The Four Ways Node.js Delivers Errors |
| io | abstract class AppError extends Error { | Custom Error Classes |
| io | const app = express(); | Express Global Error Middleware |
| CorrelationTracer.js | const asyncStore = new AsyncLocalStorage(); | Error Correlation IDs |
| ErrorWrapping.js | class DatabaseError extends Error { | Error Wrapping |
| GracefulShutdown.js | const server = http.createServer((req, res) => { | Graceful Shutdown |
| AsyncWrapper.js | const asyncWrapper = (fn) => (req, res, next) => | Async Wrapper |
| ResponseStandard.js | app.use((err, req, res, next) => { | API Response Standardization |
| routeWrapper.js | const asyncHandler = (fn) => (req, res, next) => { | Using Async-Await |
| errorGates.js | app.use('/', asyncHandler(route)); | Quick Checklist |
| errorDeliveryModes.js | function syncThrow() { throw new Error('sync'); } // 1. synchronous | Explanation |
| asyncAwaitErrors.js | const risky = async () => { throw new Error('fail'); }; | Using Async-Await |
| custom-error.js | class DatabaseError extends Error { | Error.captureStackTrace Edge Cases |
| monitor.js | process.on('uncaughtExceptionMonitor', (err, origin) => { | uncaughtExceptionMonitor Hook |
| start.sh | NODE_OPTIONS='--async-stack-traces' node app.js | --async-stack-traces Flag |
| serialize-error.js | function serializeError(err) { | Error Serialization for Logging |
| problem-error.js | class ProblemError extends Error { | Cross-Service RFC 7807 Error Propagation |
| checklist.js | const asyncHandler = (fn) => (req, res, next) => | Error Handling Checklist |
Key takeaways
this.constructor as second argument. It does not capture async context; use --async-stack-traces for that.captureStackTrace in the constructor before property assignments and validate its existence to avoid silent stack trace loss.cause property.Interview Questions on This Topic
What are the four error delivery channels in Node.js?
Frequently Asked Questions
20+ years shipping production JavaScript and front-end systems at scale. Drawn from code that ran under real load.
That's Node.js. Mark it forged?
14 min read · try the examples if you haven't