DeesseJS Errors

Exception Chaining

Chain errors together using the from() method to preserve the full context of what went wrong.

When an error occurs as a result of another error, it's important to preserve that relationship. @deessejs/errors provides the from() method for exactly this purpose, creating chains that maintain the full history of failures.

Basic Chaining

The from() method attaches a cause error to the current error. Call it on an error instance to establish the relationship:

basic.ts
import { error } from '@deessejs/errors';

const AppError = error({ name: 'AppError' });
const ValidationError = error({ name: 'ValidationError' });

// Create the original error. The no-schema form has an empty
// input shape, so the call is `ValidationError()` with no args.
const validationErr = ValidationError();

// Create the wrapping error
const appErr = AppError();

// Chain the validation error as the cause
appErr.from(validationErr);

console.log(appErr.cause === validationErr); // true

The from() method returns the error instance, so you can chain method calls or use it inline.

Multiple Causes

A single error has one direct cause at a time: each from() call replaces the previous cause with a new one. To build a chain of length N, the previous cause itself must carry its own .cause. Calling .from() repeatedly on the same error does not accumulate a list — the second call overwrites the first.

Build the chain by attaching each cause to the previous one. The causes() helper then walks the chain by following each cause's own .cause link, returning the immediate cause down to the root:

multiple.ts
import { error, causes } from '@deessejs/errors';

const AppError = error({ name: 'AppError' });
const ValidationError = error({ name: 'ValidationError' });

// Build the chain: the root cause is attached first, then
// the next layer, then the leaf. Each .from() sets the
// direct cause on the receiver.
const databaseErr = new Error('Database connection failed');
const validationErr = ValidationError().from(databaseErr);
const appErr = AppError().from(validationErr);

// Walk the chain — the order is immediate cause first, root
// cause last. causes() detects cycles.
const chain = causes(appErr);
console.log(chain.length); // 2 — [validationErr, databaseErr]
console.log(chain[0].name); // "ValidationError"
console.log(chain[1].message); // "Database connection failed"

The chain order is always immediate-cause first, root-cause last.

Chaining with raise()

You can combine from() with raise() for concise error handling:

with-raise.ts
import { error, raise, is } from '@deessejs/errors';

const AppError = error({ name: 'AppError' });
const ValidationError = error({ name: 'ValidationError' });

try {
  try {
    // Something throws a validation error.
    raise(ValidationError());
  } catch (err) {
    // err is `unknown` in strict mode; check before .from(err).
    if (err instanceof Error) {
      raise(AppError().from(err));
    } else {
      throw err;
    }
  }
} catch (err) {
  // The outer try swallows the chained throw so the
  // example terminates cleanly. In real code the raise
  // propagates to the caller.
  if (is(err, AppError)) {
    console.log(err.cause !== null); // -> true
    console.log(is(err.cause, ValidationError)); // -> true
  }
}

Since from() returns the error instance, you can write this more compactly:

compact.ts
import { error, raise, is } from '@deessejs/errors';

const AppError = error({ name: 'AppError' });
const ValidationError = error({ name: 'ValidationError' });

try {
  try {
    raise(ValidationError());
  } catch (err) {
    if (err instanceof Error) {
      raise(AppError().from(err));
    } else {
      throw err;
    }
  }
} catch (err) {
  if (is(err, AppError)) {
    console.log(err.cause !== null); // -> true
    console.log(is(err.cause, ValidationError)); // -> true
  }
}

The err instanceof Error check is required in strict mode: TypeScript types a catch clause's binding as unknown, and .from() expects an Error | ErrorInstance<any>. Without the check, the call is a type error.

Accessing the Full Chain

The causes() helper walks the chain by following each cause's own .cause link. It detects cycles and returns a new array on each call (mutations do not affect the underlying error).

access.ts
import { error, causes } from '@deessejs/errors';

const AppError = error({ name: 'AppError' });
const ValidationError = error({ name: 'ValidationError' });

// Build the chain by attaching each cause to the previous.
const root = new Error('Connection timeout');
const middle = ValidationError().from(root);
const appErr = AppError().from(middle);

// Walk the chain via the causes() helper.
const chain = causes(appErr);
console.log(chain.length); // 2 — [middle (ValidationError), root (Error)]
console.log(chain[0].name); // "ValidationError"
console.log(chain[1].message); // "Connection timeout"

causes() accepts any value: a factory-produced ErrorInstance, a native Error, or null/undefined (returns []). It also walks native Error.cause chains, so mixed native and factory errors compose.

The causes() function is particularly useful when working with errors from different sources, as it gracefully handles cases where the causes array might not exist.

Why Chain Errors?

Exception chaining serves several important purposes:

Debugging — When something goes wrong in production, the full chain tells the complete story of what happened, not just the final error.

Logging — You can log or monitor the entire chain to understand patterns in failures across your application.

User experience — Show users a meaningful error message while keeping technical details for diagnostics.

Service boundaries — When errors cross async boundaries or service calls, chaining preserves context that would otherwise be lost.

See Also

On this page