Module: Nodejs
Nodejs·120·4 MIN READ

120: Node Errors, Stack Traces, Async Failures, Assertions, and Diagnostic Context

TOPICS COVERED: 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:assert appropriately;
  • 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:

text
TypeError
ReferenceError
SyntaxError
RangeError
js
null.name;
// TypeError

System errors

Node wraps operating-system failures.

Example:

js
import { readFile } from 'node:fs/promises';

await readFile('/missing/file');

Error can include fields such as:

text
code: ENOENT
errno
syscall
path

Use stable error code where documented rather than string-matching human messages.

Assertion errors

js
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:

js
assert(user.email);

and then expose an assertion crash as API validation.

Application/domain errors

Create meaningful classes:

js
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:

text
code
status
details
cause

and a safe user message.

Error causes

js
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:

js
throw 'failed';

Prefer:

js
throw new Error('failed');

Error objects provide stack and standard behavior.

Stack traces

Example:

text
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

js
async function main() {
  throw new Error('boom');
}

await main();

At top level, catch where you can decide process behavior:

js
try {
  await main();
} catch (error) {
  console.error(error);
  process.exitCode = 1;
}

Callback errors

Node-style callbacks commonly use:

js
callback(error, value)

Example:

js
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:

js
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:

text
record safely
begin shutdown if possible
allow supervisor restart

Do not continue serving indefinitely.

Global handlers are last-resort boundaries

js
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:

text
invalid input
record not found
permission denied
business conflict
rate limit

Unexpected:

text
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:

js
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:

js
catch (error) {
  throw new Error(error.message);
}

Loses type/code/cause.

Better:

js
catch (cause) {
  throw new DatabaseUnavailableError('Could not load tasks', {
    cause,
  });
}

or rethrow unchanged if no context added.

node:assert

Test:

js
import assert from 'node:assert/strict';

assert.deepEqual(
  normalizeTask({ title: ' A ' }),
  { title: 'A' },
);

Invariant:

js
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.

js
import { AsyncLocalStorage } from 'node:async_hooks';

export const requestContext = new AsyncLocalStorage();

At request boundary:

js
requestContext.run(
  { requestId: crypto.randomUUID() },
  () => {
    handleRequest();
  },
);

Deep service:

js
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:

js
logger.error({
  err: error,
  requestId,
  taskId,
}, 'task update failed');

over:

js
console.log('ERROR ' + error);

Redact:

  • authorization headers;
  • passwords;
  • tokens;
  • sensitive user data.

Error response safety

Server should not return raw:

text
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.

js
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

js
try {
  ...
} catch {}

Swallows failure.

Only intentionally ignore errors you have explicitly classified as safe.

Catch and return null

js
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

  1. Create NotFoundError, ValidationError, and ConflictError.
  2. Wrap a filesystem error with cause.
  3. Inspect stack traces through three async functions.
  4. Convert callback API to Promise API.
  5. Build a top-level CLI error boundary.
  6. Add AsyncLocalStorage request IDs to a small HTTP server.
  7. Force an unhandled rejection in a test process and document production policy.
  8. 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.

Official references