Skip to content

@absoluteui/helpers/error

Error classes with stable codes, Result values, safe serialization, and field-error maps for forms.

import { ... } from '@absoluteui/helpers/error';

Typed errors with stable code and kind, a Result type for code that should not throw, and converters that turn anything thrown into form messages, log records, or worker messages. Every helper in the package throws these classes, so one catch handles them all. The Helpers and services guide explains the error-code conventions.

How do I turn thrown errors into form field messages?

Section titled “How do I turn thrown errors into form field messages?”

Collect the errors from a save and pass them to fieldErrors: fields from validation (and HTTP 422) errors map to their inputs, anything else becomes one form-level message under _form.

import { fieldErrors, HttpError, ValidationError } from '@absoluteui/helpers/error';
const clientCheck = new ValidationError('HRM_EMP_INVALID', 'Fix the highlighted fields.', {
fields: { email: 'Enter a valid email address.' },
});
const serverReply = new HttpError('HRM_HTTP_422', 'The request was rejected.', {
status: 422,
fields: { startDate: 'The start date is in a closed payroll period.' },
});
const messages = fieldErrors([clientCheck, serverReply, new Error('Server unavailable.')]);
messages; // => { email: 'Enter a valid email address.', startDate: 'The start date is in a closed payroll period.', _form: 'Server unavailable.' }

How do I decide what to show for a failed call?

Section titled “How do I decide what to show for a failed call?”

Branch on kind or code, never on the message. isHelperError narrows an unknown catch value; HttpError adds category and retryable.

import { HttpError, isHelperError, NotFoundError, toError } from '@absoluteui/helpers/error';
function userMessage(caught: unknown): string {
if (caught instanceof HttpError && caught.retryable) return 'The service is busy. Try again shortly.';
if (isHelperError(caught)) {
if (caught.kind === 'not-found') return 'This employee no longer exists.';
if (caught.kind === 'conflict') return 'Someone else changed this record. Reload and try again.';
}
return toError(caught).message;
}
userMessage(new NotFoundError('HRM_EMP_NOT_FOUND', 'Employee not found.')); // => 'This employee no longer exists.'
userMessage(new HttpError('HRM_HTTP_503', 'Service unavailable.', { status: 503 })); // => 'The service is busy. Try again shortly.'
userMessage('boom'); // => 'A non-error value was thrown.'

How do I validate rows without try/catch everywhere?

Section titled “How do I validate rows without try/catch everywhere?”

Return a Result with ok/err, wrap throwing code with tryCatch, and use combineResults to stop at the first bad row.

import { combineResults, err, ok, ValidationError, type Result } from '@absoluteui/helpers/error';
function parseHours(row: string, line: number): Result<number, ValidationError> {
const hours = Number(row);
return Number.isFinite(hours) && hours >= 0 && hours <= 24
? ok(hours)
: err(new ValidationError('HRM_TIMESHEET_HOURS', `Line ${line}: hours must be 0 to 24.`));
}
combineResults(['8', '7.5'].map((r, i) => parseHours(r, i + 1))); // => { ok: true, value: [8, 7.5] }
const bad = combineResults(['8', '30', 'x'].map((r, i) => parseHours(r, i + 1)));
bad.ok ? '' : bad.error.message; // => 'Line 2: hours must be 0 to 24.'

How do I send an error to a log or across a worker boundary?

Section titled “How do I send an error to a log or across a worker boundary?”

serializeError produces a JSON-safe object (with the cause chain, without stacks by default); deserializeError rebuilds a typed error on the other side.

import { ConflictError, deserializeError, serializeError } from '@absoluteui/helpers/error';
const original = new ConflictError('HRM_EMP_VERSION', 'The record was changed by someone else.');
const wire = JSON.stringify(serializeError(original));
const restored = deserializeError(JSON.parse(wire));
[restored.name, restored.code, restored.kind]; // => ['ConflictError', 'HRM_EMP_VERSION', 'conflict']

Messages are shown to users and written to logs, so never put secrets or raw input in them; put structured, non-sensitive context in details. invariant guards internal assumptions and throws HLP_ASSERT:

import { invariant, unwrap, err, NotFoundError } from '@absoluteui/helpers/error';
const leaveBalance = -2;
invariant(leaveBalance >= 0, 'Leave balance must not be negative.'); // => throws HLP_ASSERT
unwrap(err(new NotFoundError('HRM_EMP_NOT_FOUND', 'Employee not found.'))); // => throws HRM_EMP_NOT_FOUND

assertNever

Exhaustiveness check for switch statements.

function assertNever(value: never, message?: string): never;

Example:

type Shift = 'day' | 'night';
function label(shift: Shift): string {
switch (shift) {
case 'day': return 'Day shift';
case 'night': return 'Night shift';
default: return assertNever(shift); // compile error if a Shift is not handled
}
}
label('night'); // => 'Night shift'

combineResults

Collects results: all values, or the first error.

function combineResults<T, E>(results: readonly Result<T, E>[]): Result<T[], E>;

Example:

combineResults([ok(1), ok(2)]); // => { ok: true, value: [1, 2] }
combineResults([ok(1), err('E_ROW_2'), err('E_ROW_3')]); // => { ok: false, error: 'E_ROW_2' }

deserializeError

Rebuilds a HelperError from serializeError output (for example, received from a worker). Unknown shapes become HLP_INVALID_SERIALIZED_ERROR.

function deserializeError(value: unknown): HelperError;

Example:

const e = deserializeError({ name: 'NotFoundError', message: 'Employee not found.', code: 'HRM_EMP_NOT_FOUND', kind: 'not-found' });
[e.name, e.code, e.kind]; // => ['NotFoundError', 'HRM_EMP_NOT_FOUND', 'not-found']
deserializeError(42).code; // => 'HLP_INVALID_SERIALIZED_ERROR'

err

function err<E>(error: E): Err<E>;

Example:

const r = err(new ValidationError('HRM_LEAVE_RANGE', 'The end date is before the start date.'));
r.ok; // => false
r.error.code; // => 'HRM_LEAVE_RANGE'

fieldErrors

Field-error map for form error summaries (error-summary, form-field). Collects fields from ValidationErrors; other errors go under formKey.

function fieldErrors(errors: readonly unknown[], formKey?: string): Record<string, string>;

Example:

const errors = [new ValidationError('HLP_V', 'Invalid.', { fields: { email: 'Enter a valid email address.' } }), new Error('Server unavailable.')];
fieldErrors(errors); // => { email: 'Enter a valid email address.', _form: 'Server unavailable.' }

httpFailureCategory

Failure category of a status: specific statuses first, then client (4xx) or server (5xx).

function httpFailureCategory(status: number): HttpFailureCategory;

Example:

httpFailureCategory(422) // => 'validation'

Example:

httpFailureCategory(503) // => 'unavailable'

Example:

httpFailureCategory(418) // => 'client'

invariant

Throws a HelperError with code HLP_ASSERT when condition is falsy.

function invariant(condition: unknown, message?: string, code?: string): asserts condition;

Example:

const budget = -5;
invariant(budget >= 0, 'Budget must not be negative.'); // => throws HLP_ASSERT

isAbortError

True for any abort, from AbortController, DOM, or this package.

function isAbortError(value: unknown): boolean;

Example:

isAbortError(new DOMException('Stopped.', 'AbortError')); // => true
isAbortError(new TimeoutError()); // => false

isHelperError

True when value is a HelperError, optionally with the given code.

function isHelperError(value: unknown, code?: string): value is HelperError;

Example:

const e = new ConflictError('HRM_EMP_VERSION', 'The record was changed by someone else.');
isHelperError(e, 'HRM_EMP_VERSION'); // => true
isHelperError(new Error('Plain.')); // => false

mapError

Maps the error value.

function mapError<T, E, F>(result: Result<T, E>, fn: (error: E) => F): Result<T, F>;

Example:

mapError(err(404), (status) => `HTTP ${status}`); // => { ok: false, error: 'HTTP 404' }

mapResult

Maps the success value.

function mapResult<T, U, E>(result: Result<T, E>, fn: (value: T) => U): Result<U, E>;

Example:

mapResult(ok(20), (days) => days * 8); // => { ok: true, value: 160 }

ok

function ok<T>(value: T): Ok<T>;

Example:

const r = ok(42); // => { ok: true, value: 42 }

serializeError

Converts an error (and its cause chain) into a JSON-safe object.

function serializeError(value: unknown, options?: SerializeErrorOptions): SerializedError;

Example:

const e = new HttpError('HLP_HTTP_503', 'Service unavailable.', { status: 503 });
serializeError(e); // => { name: 'HttpError', message: 'Service unavailable.', code: 'HLP_HTTP_503', kind: 'http', status: 503 }

toError

Normalizes anything thrown into an Error. Non-error values become a HelperError with code HLP_NON_ERROR_THROWN and the value kept only as cause.

function toError(value: unknown): Error;

Example:

toError(new RangeError('Out of range.')).message; // => 'Out of range.'
isHelperError(toError('boom'), 'HLP_NON_ERROR_THROWN'); // => true

tryCatch

Runs fn, capturing a throw as Err.

function tryCatch<T>(fn: () => T): Result<T>;

Example:

const r = tryCatch(() => JSON.parse('{"id":1}')); // => { ok: true, value: { id: 1 } }
tryCatch(() => JSON.parse('{')).ok; // => false

tryCatchAsync

Awaits fn, capturing a rejection as Err.

function tryCatchAsync<T>(fn: () => Promise<T> | T): Promise<Result<T>>;

Example:

const r = await tryCatchAsync(async () => { throw new TimeoutError(); });
r.ok; // => false

unwrap

Returns the value or throws the error.

function unwrap<T, E>(result: Result<T, E>): T;

Example:

unwrap(ok(5)); // => 5
unwrap(err(new NotFoundError('HRM_EMP_NOT_FOUND', 'Employee not found.'))); // => throws HRM_EMP_NOT_FOUND

unwrapOr

Returns the value or fallback.

function unwrapOr<T, E>(result: Result<T, E>, fallback: T): T;

Example:

unwrapOr(tryCatch(() => JSON.parse('{')), {}); // => {}

AbortError

class AbortError extends HelperError {
name: string;
constructor(code?: string, message?: string, options?: HelperErrorOptions);
}

Example:

const e = new AbortError();
e.code; // => 'HLP_ABORTED'
isAbortError(e); // => true

ConflictError

class ConflictError extends HelperError {
name: string;
constructor(code: string, message: string, options?: HelperErrorOptions);
}

Example:

new ConflictError('HRM_EMP_VERSION', 'The record was changed by someone else.').kind; // => 'conflict'

HelperError

Base error with a stable machine-readable code.

class HelperError extends Error {
name: string;
readonly code: string;
readonly kind: ErrorKind;
readonly details: Readonly<Record<string, unknown>>;
readonly fields: Readonly<Record<string, string>>;
constructor(code: string, message: string, options?: HelperErrorOptions & { readonly kind?: ErrorKind; });
}

Example:

const e = new HelperError('HLP_JSON_TOO_DEEP', 'JSON nesting exceeds the limit.', { kind: 'limit' });
e.code; // => 'HLP_JSON_TOO_DEEP'
e.kind; // => 'limit'
e instanceof Error; // => true

HttpError

A failed HTTP call: an unsuccessful status, or no response at all (status 0 with category network, offline, timeout, or cancelled). message is the user-facing message.

class HttpError extends HelperError {
name: string;
readonly status: number;
readonly statusText: string;
readonly problem: Readonly<Record<string, unknown>> | undefined;
readonly method: string | undefined;
readonly url: string | undefined;
readonly category: HttpFailureCategory;
readonly retryable: boolean;
readonly retryAfterMs: number | undefined;
readonly requestId: string | undefined;
readonly body: unknown;
constructor(code: string, message: string, options: HttpErrorOptions);
toJSON(): HttpErrorJson;
}

Example:

const e = new HttpError('HLP_HTTP_404', 'Not found.', { status: 404, method: 'GET', url: '/api/employees/42' });
[e.status, e.kind, e.category, e.retryable]; // => [404, 'not-found', 'not-found', false]

Example:

const e = new HttpError('SVC_HTTP_STATUS', 'Too many requests.', { status: 429, retryAfterMs: 2000, requestId: 'req-7' });
[e.category, e.retryable, e.retryAfterMs]; // => ['rate-limited', true, 2000]
JSON.parse(JSON.stringify(e)).requestId; // => 'req-7'

LimitError

A limit (size, depth, count, quota) was exceeded.

class LimitError extends HelperError {
name: string;
constructor(code: string, message: string, options?: HelperErrorOptions);
}

Example:

const e = new LimitError('HRM_IMPORT_TOO_LARGE', 'The import has too many rows.', { details: { max: 10000 } });
[e.kind, e.details]; // => ['limit', { max: 10000 }]

NotFoundError

class NotFoundError extends HelperError {
name: string;
constructor(code: string, message: string, options?: HelperErrorOptions);
}

Example:

new NotFoundError('HRM_EMP_NOT_FOUND', 'Employee not found.').kind; // => 'not-found'

TimeoutError

class TimeoutError extends HelperError {
name: string;
constructor(code?: string, message?: string, options?: HelperErrorOptions);
}

Example:

const e = new TimeoutError();
[e.code, e.kind, e.message]; // => ['HLP_TIMEOUT', 'timeout', 'The operation timed out.']

ValidationError

Input failed validation. fields maps field paths to messages.

class ValidationError extends HelperError {
name: string;
constructor(code: string, message: string, options?: HelperErrorOptions);
}

Example:

const e = new ValidationError('HRM_EMP_INVALID', 'Fix the highlighted fields.', { fields: { email: 'Enter a valid email address.' } });
e.kind; // => 'validation'
e.fields; // => { email: 'Enter a valid email address.' }

Err

interface Err<E> {
readonly ok: false;
readonly error: E;
}

HelperErrorOptions

interface HelperErrorOptions {
readonly cause?: unknown;
readonly details?: Readonly<Record<string, unknown>>;
readonly fields?: Readonly<Record<string, string>>;
}

HttpErrorJson

Log-safe summary of an HttpError (JSON.stringify uses it).

interface HttpErrorJson {
readonly name: string;
readonly code: string;
readonly message: string;
readonly status: number;
readonly category: HttpFailureCategory;
readonly retryable: boolean;
readonly method: string | undefined;
readonly url: string | undefined;
readonly requestId: string | undefined;
readonly retryAfterMs: number | undefined;
readonly fields: Readonly<Record<string, string>>;
readonly problemType: string | undefined;
}

HttpErrorOptions

interface HttpErrorOptions extends HelperErrorOptions {
readonly status: number;
readonly statusText?: string;
readonly problem?: Readonly<Record<string, unknown>>;
readonly method?: string;
readonly url?: string;
readonly category?: HttpFailureCategory;
readonly retryable?: boolean;
readonly retryAfterMs?: number;
readonly requestId?: string;
readonly body?: unknown;
}

Ok

interface Ok<T> {
readonly ok: true;
readonly value: T;
}

SerializedError

Plain, JSON-safe representation of an error.

interface SerializedError {
readonly name: string;
readonly message: string;
readonly code?: string;
readonly kind?: ErrorKind;
readonly status?: number;
readonly details?: Readonly<Record<string, unknown>>;
readonly fields?: Readonly<Record<string, string>>;
readonly stack?: string;
readonly cause?: SerializedError;
}

SerializeErrorOptions

interface SerializeErrorOptions {
readonly includeStack?: boolean;
readonly maxDepth?: number;
}

ErrorKind

Stable error category used for mapping errors to UI and HTTP responses.

type ErrorKind = 'validation' | 'not-found' | 'conflict' | 'http' | 'timeout' | 'aborted' | 'unauthorized' | 'forbidden' | 'unsupported' | 'limit' | 'internal';

HttpFailureCategory

Why an HTTP call failed, for choosing a message, a retry, or a sign-in flow. client covers the 4xx statuses without a more specific category.

type HttpFailureCategory = 'network' | 'offline' | 'timeout' | 'cancelled' | 'unauthorized' | 'forbidden' | 'not-found' | 'conflict' | 'validation' | 'rate-limited' | 'client' | 'server' | 'unavailable' | 'bad-response' | 'too-large';

Result

Discriminated success/failure value for code paths that must not throw.

type Result<T, E = Error> = Ok<T> | Err<E>;