Skip to main content

Import

import {
  ErrorBoundary,
  AppError,
  AppValidationError,
  NotFoundError,
  UnauthorizedError,
  ForbiddenError,
  ConflictError,
  RateLimitError,
  TimeoutError,
  getGlobalErrorBoundary,
  resetGlobalErrorBoundary,
} from "bytekit/error-boundary";

What it does

ErrorBoundary provides a centralized place to handle, normalize, retry, and log errors. Plain Error instances are automatically converted into typed AppError subclasses using keyword detection, so you always work with structured errors that carry a status code, error code, and context.

ErrorBoundary

Constructor

const boundary = new ErrorBoundary(config?: ErrorBoundaryConfig);

ErrorBoundaryConfig

PropertyTypeDefaultDescription
loggerLoggerLogger instance for error output.
maxRetriesnumber3Max retries for execute().
retryDelaynumber1000Delay between retries (ms).
handlersErrorHandler[][]Initial error handler functions.
onError(error, context) => voidCallback invoked on every error.
onErrorRecovery(error, context) => voidCallback invoked on successful recovery.
isDevelopmentbooleanEnables extra diagnostics in dev mode.

Methods

MethodSignatureDescription
handlehandle(error, context?): Promise<void>Normalize, log, and dispatch to all handlers.
executeexecute<T>(fn, context?, retries?): Promise<T>Run async function with error handling and retries.
executeSyncexecuteSync<T>(fn, context?): TRun sync function with error handling.
wrapwrap<T>(fn, context?): TReturn a wrapped version of an async function that handles errors.
wrapSyncwrapSync<T>(fn, context?): TReturn a wrapped version of a sync function that handles errors.
addHandleraddHandler(handler): voidRegister an error handler.
removeHandlerremoveHandler(handler): voidRemove a registered handler.
clearHandlersclearHandlers(): voidRemove all handlers.
getErrorHistorygetErrorHistory(limit?): ErrorRecord[]Get recent errors (default last 10).
clearErrorHistoryclearErrorHistory(): voidClear the error history stack.
createErrorReportcreateErrorReport(): ErrorReportGenerate a timestamped report of all recorded errors.

ErrorContext

PropertyTypeDescription
componentstringComponent or module name.
contextstringFree-form context string.
userIdstringUser identifier.
sessionIdstringSession identifier.
metadataRecord<string, unknown>Arbitrary metadata.
originalErrorErrorThe original error before normalization.

Error classes

All error classes extend AppError, which extends Error.

AppError

new AppError(code, message, statusCode?, context?, originalError?)
PropertyTypeDescription
codestringMachine-readable error code (e.g. "NOT_FOUND").
messagestringHuman-readable message.
statusCodenumberHTTP status code (default 500).
contextErrorContextAdditional context.
originalErrorErrorThe original error, if normalized.

Typed error subclasses

ClassCodeStatusNotes
AppValidationErrorVALIDATION_ERROR400Validation failures.
UnauthorizedErrorUNAUTHORIZED401Authentication required.
ForbiddenErrorFORBIDDEN403Insufficient permissions.
NotFoundErrorNOT_FOUND404Resource not found.
TimeoutErrorTIMEOUT408Operation timed out.
ConflictErrorCONFLICT409Resource conflict.
RateLimitErrorRATE_LIMIT429Too many requests. Accepts optional retryAfter (stored in metadata).

Automatic error normalization

When ErrorBoundary receives a plain Error, it inspects the message and converts it to the closest typed subclass:
Keyword in messageNormalized to
"timeout" / "Timeout"TimeoutError
"validation" / "Validation"AppValidationError
"not found" / "Not Found"NotFoundError
(anything else)AppError with code UNKNOWN_ERROR

Retryable errors

ErrorBoundary.execute() automatically retries when isRetryable returns true:
  • Status 408 (timeout)
  • Status 429 (rate limit)
  • Status 5xx (server errors)
Non-retryable errors (like 400, 401, 403, 404, 409) fail immediately without retrying.

Examples

Basic error handling

import { ErrorBoundary } from "bytekit/error-boundary";

const boundary = new ErrorBoundary({
  maxRetries: 2,
  retryDelay: 500,
});

const data = await boundary.execute(
  () => fetchUserProfile(userId),
  { context: "profile-load", userId },
);

Wrapping functions

const safeFetch = boundary.wrap(
  async (id: string) => api.get(`/users/${id}`),
  { component: "UserService" },
);

// Errors are automatically caught, normalized, and dispatched
const user = await safeFetch("123");

Custom error handlers

const boundary = new ErrorBoundary({
  onError: (error, context) => {
    analytics.track("error", {
      code: error instanceof AppError ? error.code : "UNKNOWN",
      component: context.component,
    });
  },
  handlers: [
    async (error, context) => {
      if (error instanceof RateLimitError) {
        showToast("Too many requests — please wait.");
      }
    },
  ],
});

Throwing typed errors

import { NotFoundError, AppValidationError } from "bytekit/error-boundary";

function getUser(id: string) {
  const user = db.find(id);
  if (!user) throw new NotFoundError(`User ${id} not found`);
  return user;
}

function createUser(data: unknown) {
  if (!data || typeof data !== "object") {
    throw new AppValidationError("Invalid user data");
  }
  // ...
}

Error report

const report = boundary.createErrorReport();
console.log(report);
// {
//   timestamp: "2026-03-27T...",
//   errors: [
//     { code: "NOT_FOUND", message: "...", statusCode: 404, ... },
//   ],
// }

Global error boundary

A singleton ErrorBoundary for app-wide error handling:
import {
  getGlobalErrorBoundary,
  resetGlobalErrorBoundary,
} from "bytekit/error-boundary";

const boundary = getGlobalErrorBoundary({ maxRetries: 2 });

// Reset the global instance (e.g. in tests)
resetGlobalErrorBoundary();
getGlobalErrorBoundary() returns the same instance on every call. Pass a config only on the first call — subsequent calls ignore the config argument.
The global boundary registers unhandledrejection and onerror listeners automatically. Call resetGlobalErrorBoundary() in test teardown to avoid listener leaks.