120: Node Errors, Stack Traces, Async Failures, Assertions, and Diagnostic Context
Learning objectives
You will learn to:
- distinguish JavaScript errors, system errors, assertion errors, and application/domain errors;
- create error classes with useful metadata;
- preserve error causes;
- handle Promise rejections and callback errors;
- understand uncaught exceptions and unhandled rejections;
- read stack traces;
- use
node:assertappropriately; - use source maps;
- attach request context safely with AsyncLocalStorage;
- design error boundaries for CLI, worker, and server processes;
- avoid swallowing failures.
Error taxonomy
JavaScript errors
Examples:
TypeError ReferenceError SyntaxError RangeError
null.name;
// TypeError
System errors
Node wraps operating-system failures.
Example:
import { readFile } from 'node:fs/promises';
await readFile('/missing/file');
Error can include fields such as:
code: ENOENT errno syscall path
Use stable error code where documented rather than string-matching human messages.
Assertion errors
import assert from 'node:assert/strict';
assert.equal(2 + 2, 4);
Assertions are excellent in tests and internal invariants.
Do not use assertions as user input validation:
assert(user.email);
and then expose an assertion crash as API validation.
Application/domain errors
Create meaningful classes:
export class NotFoundError extends Error {
constructor(resource, options = {}) {
super(`${resource} was not found`, options);
this.name = 'NotFoundError';
this.code = 'NOT_FOUND';
this.status = 404;
}
}
Use machine-readable fields:
code status details cause
and a safe user message.
Error causes
try {
await readFile(configPath, 'utf8');
} catch (cause) {
throw new Error('Could not load application configuration', {
cause,
});
}
This preserves underlying failure.
Logging can walk the cause chain while the caller receives meaningful context.
Do not concatenate everything into one string and lose structured cause.
Throwing non-Error values
Avoid:
throw 'failed';
Prefer:
throw new Error('failed');
Error objects provide stack and standard behavior.
Stack traces
Example:
Error: Could not load task at getTask (.../task-service.js:42:11) at async handler (.../routes.js:18:16)
Read from top application frames outward.
Questions:
- where was error created?
- where did async call originate?
- is the top frame generated/transpiled?
- is there a cause?
Source maps
If code is transpiled/bundled, source maps map generated stack locations to source.
Modern Node supports source-map options/features.
Protect production source maps appropriately if they contain source not meant for public exposure.
Server-side map files can remain available to error tooling without being web-served.
Promise failures
async function main() {
throw new Error('boom');
}
await main();
At top level, catch where you can decide process behavior:
try {
await main();
} catch (error) {
console.error(error);
process.exitCode = 1;
}
Callback errors
Node-style callbacks commonly use:
callback(error, value)
Example:
import { readFile } from 'node:fs';
readFile('data.json', 'utf8', (error, text) => {
if (error) {
console.error(error);
return;
}
console.log(text);
});
Never continue to use text after an error unless API contract says it is valid.
Promise APIs are often easier for new application code.
promisify
Legacy callback API:
import { promisify } from 'node:util';
const legacyAsync = promisify(legacyFunction);
Prefer first-party Promise API such as node:fs/promises where available.
Unhandled rejection
A Promise rejects with no handler.
This is a programmer/lifecycle defect.
Do not install a global handler merely to suppress it.
At process level, observe/log and terminate according to your service policy.
Fix the missing handling at source.
Uncaught exception
A synchronous exception reaches the event loop without a catch.
At that point application state can be unknown.
Recommended production mindset:
record safely begin shutdown if possible allow supervisor restart
Do not continue serving indefinitely.
Global handlers are last-resort boundaries
process.on('uncaughtException', (error) => {
logger.fatal({ error }, 'uncaught exception');
beginShutdown();
});
process.on('unhandledRejection', (reason) => {
logger.fatal({ reason }, 'unhandled rejection');
beginShutdown();
});
Implementation must guard against re-entrant shutdown.
Do not perform long unreliable async recovery from a corrupted process.
Expected versus unexpected errors
Expected:
invalid input record not found permission denied business conflict rate limit
Unexpected:
null dereference invariant violation database driver bug coding error
Expected errors should map to controlled API/CLI behavior.
Unexpected errors should be observed and contained at boundaries.
Result versus throw
Not every negative outcome must throw.
Parsing user input:
function parsePort(raw) {
const value = Number(raw);
if (!Number.isInteger(value)) {
return {
ok: false,
error: 'PORT must be an integer',
};
}
return { ok: true, value };
}
Can be clearer than exceptions for ordinary validation.
Use throws for exceptional control flow appropriate to your architecture.
Error wrapping without losing identity
Bad:
catch (error) {
throw new Error(error.message);
}
Loses type/code/cause.
Better:
catch (cause) {
throw new DatabaseUnavailableError('Could not load tasks', {
cause,
});
}
or rethrow unchanged if no context added.
node:assert
Test:
import assert from 'node:assert/strict';
assert.deepEqual(
normalizeTask({ title: ' A ' }),
{ title: 'A' },
);
Invariant:
assert.ok(config.port > 0);
For public runtime inputs, return controlled validation errors instead.
AsyncLocalStorage
Servers often need a request/correlation ID available across async calls.
import { AsyncLocalStorage } from 'node:async_hooks';
export const requestContext = new AsyncLocalStorage();
At request boundary:
requestContext.run(
{ requestId: crypto.randomUUID() },
() => {
handleRequest();
},
);
Deep service:
const context = requestContext.getStore();
logger.info(
{ requestId: context?.requestId },
'loading task',
);
This avoids passing request ID through every function signature purely for diagnostics.
Do not use AsyncLocalStorage to hide core business dependencies like current user authorization if explicit arguments make security clearer.
Structured logging
Prefer:
logger.error({
err: error,
requestId,
taskId,
}, 'task update failed');
over:
console.log('ERROR ' + error);
Redact:
- authorization headers;
- passwords;
- tokens;
- sensitive user data.
Error response safety
Server should not return raw:
stack trace SQL/Mongo query internals file paths environment details
to an untrusted client.
Return stable public error code/message and log internal detail securely.
Error handling in cleanup
Cleanup can fail too.
try {
await server.close();
} catch (error) {
logger.error({ error }, 'server close failed');
}
During shutdown, continue attempting independent cleanup within a deadline.
AggregateError
Multiple concurrent failures can be represented by AggregateError.
Promise.any can reject with one.
Batch systems may deliberately aggregate errors.
UI/API boundaries should decide how much detail is safe to expose.
Error monitoring
Production systems benefit from:
- error tracking;
- structured logs;
- request correlation;
- metrics;
- traces/APM.
A stack trace without request context can be insufficient for distributed systems.
Failure clinic
Empty catch
try {
...
} catch {}
Swallows failure.
Only intentionally ignore errors you have explicitly classified as safe.
Catch and return null
catch {
return null;
}
Now “not found” and “database down” look identical.
Leaking raw errors to API
Security/UX issue.
Global handler continues process
May continue corrupted state.
Logging secrets
Incident becomes data leak.
Exercises
- Create
NotFoundError,ValidationError, andConflictError. - Wrap a filesystem error with
cause. - Inspect stack traces through three async functions.
- Convert callback API to Promise API.
- Build a top-level CLI error boundary.
- Add AsyncLocalStorage request IDs to a small HTTP server.
- Force an unhandled rejection in a test process and document production policy.
- Design public vs internal error shapes.
Mastery checklist
Explain:
- error taxonomy;
- system error codes;
- causes;
- async failure handling;
- unhandled rejection;
- uncaught exception;
- assertions;
- stack/source maps;
- AsyncLocalStorage;
- structured safe logging;
- expected versus unexpected errors.
