Skip to main content
jg-packages

Search

    Errors

    jg-exception

    Coded errors for every jg package: an Exception with a stable code and JSON data, message extraction, code checks, and test assertions.

    v1.5.0UniversalESMMIT

    npm install jg-exception

    Usage

    Throw an Exception with a stable code, and optional data that helps whoever handles or debugs it:

    ts
    import { Exception } from 'jg-exception';
    
    throw new Exception('The user was not found.', { code: 'USER_NOT_FOUND', data: { id } });

    Wrap the error underneath as the cause. The message says what failed at this step, and the cause keeps the rest:

    ts
    try {
      await loadUser(id);
    } catch (error) {
      throw new Exception("Couldn't load the user.", { code: 'USER_LOAD_FAILED', cause: error });
    }

    Read the code back wherever the error ends up. It works on any error with a string code, such as a Node.js system error:

    ts
    import { getErrorCode, hasErrorCode, hasErrorCodePrefix, isException } from 'jg-exception';
    
    hasErrorCode(error, 'USER_NOT_FOUND'); // true or false
    hasErrorCodePrefix(error, 'USER_'); // true for any USER_ code
    getErrorCode(error); // 'USER_NOT_FOUND', or null when the error has no code
    isException(error); // true, even for an Exception from another installed copy of the package

    An Exception turns into JSON with its name, code, message, and data, so it can be logged or sent as is:

    ts
    JSON.stringify(
      new Exception('The user was not found.', { code: 'USER_NOT_FOUND', data: { id: 1 } }),
    );
    // '{"name":"Exception","code":"USER_NOT_FOUND","message":"The user was not found.","data":{"id":1}}'
    
    String(error); // 'Exception [USER_NOT_FOUND]: The user was not found.'

    An app’s throw sites can also say how serious a failure is, for the code that reports it: severity is 'expected' or 'critical', and shouldLog: true asks for the error to be logged in full. Neither is serialized, and packages never set them:

    ts
    throw new Exception('The user was not found.', { code: 'USER_NOT_FOUND', severity: 'expected' });

    data must be a JSON value, so a BigInt, a Date, or a class instance is a type error. An object’s values may be undefined, so { email } works when email is optional. An interface has no index signature, so pass an interface-typed value as { ...user }, or declare its type with type. Whatever can’t be serialized at runtime, such as an object that refers back to itself, becomes null in toJSON(), and NaN and Infinity become null as in any JSON.

    Extract a readable message from anything a catch block receives: strings, errors with their causes, ZodErrors, and objects with a message, error, or reason:

    ts
    import { extractMessage } from 'jg-exception';
    
    extractMessage(new Error('Request failed', { cause: new Error('Timed out') }));
    // 'Request failed; [CAUSE]: Timed out'

    Turn an error into what users read with createErrorFeedback: the copy you wrote for its code, or a fallback, never the error’s message, which is written for whoever debugs it:

    ts
    import { createErrorFeedback } from 'jg-exception';
    
    const getErrorFeedback = createErrorFeedback({
      fallbackMessage: 'Something went wrong.',
      messagesByCode: { SESSION_EXPIRED: 'Your session expired. Sign in again to continue.' },
    });
    
    getErrorFeedback(error).message; // the copy for the error's code, or the fallback

    Assert error codes in tests with any test runner:

    ts
    import { expectToRejectCode, expectToThrowCode } from 'jg-exception/testing';
    
    expectToThrowCode(() => parseCursor('%%'), 'INVALID_CURSOR');
    await expectToRejectCode(loadUser('missing'), 'USER_NOT_FOUND', 'not found');

    Keeping secrets out of an error

    The error underneath can hold a secret in its message, stack, or fields, such as the token a request was sent with, and logs, console.log, and error trackers print the whole chain. When you wrap it, pass that value as redact. It’s replaced by [redacted] throughout the new exception: its message and data, and each cause’s message, stack, and fields, all the way down:

    ts
    try {
      await verifyToken(token);
    } catch (error) {
      throw new Exception("Couldn't verify the token.", {
        code: 'TOKEN_INVALID',
        cause: error,
        redact: [token],
      });
    }
    • Pass whole secrets. Each value is matched as written and URL-encoded, so a token in a request URL is found too. A fragment as short as at would also rewrite names and every stack frame. A value transformed another way, such as base64-encoded or hashed, isn’t found, so pass each form a cause may hold.
    • What it costs: each cause becomes a copy that keeps its name, code, message, stack, and plain fields, redacted, and leaves out fields holding objects, such as an HTTP client’s config. The copy runs none of the cause’s own code, such as a custom inspect hook, which could hold a value no redaction reaches. So instanceof the cause’s original class no longer holds: read its name and code instead. An Exception cause stays an Exception, so isException, hasErrorCode, and toJSON() work on it.
    • The values are used while the exception is built, and kept nowhere.

    To log an error you caught, without rethrowing it, extractRedactedMessage gives its message, causes included, with the values redacted:

    ts
    import { extractRedactedMessage } from 'jg-exception';
    
    extractRedactedMessage(error, [apiKey]); // the message with the key replaced by [redacted]

    When what you report carries the client’s data, such as the body of the request that failed, the secrets are in fields you know by name. redactFields copies it with those fields’ values replaced by [redacted], at any depth and whatever their case:

    ts
    import { redactFields } from 'jg-exception';
    
    redactFields(request.body, ['password', 'token']); // { email: '…', password: '[redacted]' }

    Generated from TSDoc

    API reference

    Every export of [email protected], 23 in all, with its signature, its errors, and an example.

    jg-exception

    Functionsjg-exception

    createErrorFeedback
    function

    Creates the function a UI turns errors into feedback with: the copy users read for the error's code, the code itself when it's safe to show, and whether the error means the user cancelled the work or isn't allowed to do it. Configure it once, and pass overrides where a screen needs copy of its own. A code is shown only when a table gives it copy or publicCodePattern matches it, so a new or internal code never reaches users, and the error's message, written for whoever debugs it, is never shown.

    Signature
    createErrorFeedback(
      options: IErrorFeedbackOptions,
    ): (error: unknown, overrides?: IErrorFeedbackOverrides) => IErrorFeedback

    Parameters

    optionsIErrorFeedbackOptions

    The fallback copy, and optionally the copy by code, the access-denied codes, and the pattern of codes that are safe to show.

    Returns

    (error: unknown, overrides?: IErrorFeedbackOverrides) => IErrorFeedback

    A function that gives an error's feedback, with optional overrides for one call. It never throws, whatever error it's given.

    Example 1
    import { Exception, createErrorFeedback } from 'jg-exception';
    
    const getErrorFeedback = createErrorFeedback({
      fallbackMessage: 'Something went wrong. Try again in a moment.',
      messagesByCode: { SESSION_EXPIRED: 'Your session expired. Sign in again to continue.' },
      accessDeniedCodes: ['SESSION_EXPIRED'],
    });
    const feedback = getErrorFeedback(new Exception('Expired', { code: 'SESSION_EXPIRED' }));
    feedback.message; // => 'Your session expired. Sign in again to continue.'
    feedback.code; // => 'SESSION_EXPIRED'
    feedback.isAccessDenied; // => true
    
    // an error without copy gets the fallback, and its code stays hidden
    getErrorFeedback(new Exception('Disk full', { code: 'DISK_FULL' })).code; // => null
    Example 2
    import { Exception, createErrorFeedback } from 'jg-exception';
    
    const getErrorFeedback = createErrorFeedback({
      fallbackMessage: 'Something went wrong.',
      // codes such as AVATAR_TOO_LARGE are safe to show next to the fallback, for support requests
      publicCodePattern: /^AVATAR_/,
    });
    const error = new Exception('The avatar is larger than 5 MB.', { code: 'AVATAR_TOO_LARGE' });
    getErrorFeedback(error).code; // => 'AVATAR_TOO_LARGE'
    
    // one screen gives the code copy of its own
    const overrides = { messagesByCode: { AVATAR_TOO_LARGE: 'Choose an image of 5 MB or less.' } };
    getErrorFeedback(error, overrides).message; // => 'Choose an image of 5 MB or less.'
    extractMessage
    function

    Extracts a message from an error of any shape: a string, an Error (with its causes), a ZodError, or an object holding the message in a common property such as message, error, or reason. Other objects are stringified. When nothing can be extracted, it returns the default message.

    Signature
    extractMessage(error: unknown): string

    Parameters

    errorunknown

    The error to extract the message from.

    Returns

    string

    The extracted message, or DEFAULT_MESSAGE.

    Example
    import { DEFAULT_MESSAGE, extractMessage } from 'jg-exception';
    
    extractMessage(new Error('Not found')); // => 'Not found'
    extractMessage({ reason: 'Timed out' }); // => 'Timed out'
    extractMessage(undefined) === DEFAULT_MESSAGE; // => true
    extractRedactedMessage
    function

    Extracts an error message and replaces every exact, case-sensitive occurrence of the sensitive values with [redacted].

    Signature
    extractRedactedMessage(error: unknown, sensitiveValues: string[]): string

    Parameters

    errorunknown

    The error to extract the message from.

    sensitiveValuesstring[]

    The exact values to redact, such as tokens or passwords.

    Returns

    string

    The extracted message with the sensitive values redacted.

    Example
    import { extractRedactedMessage } from 'jg-exception';
    
    extractRedactedMessage(new Error('Bad token abc123'), ['abc123']); // => 'Bad token [redacted]'
    getErrorCode
    function

    Reads an error's code: the non-empty string in its code property, as on an Exception, a Node.js system error, or a provider's error object.

    Signature
    getErrorCode(error: unknown): string | null

    Parameters

    errorunknown

    The error to read, of any shape.

    Returns

    string | null

    The code, or null when the error has none.

    Example
    import { Exception, getErrorCode } from 'jg-exception';
    
    getErrorCode(new Exception('Not found', { code: 'NOT_FOUND' })); // => 'NOT_FOUND'
    getErrorCode(new Error('Not found')); // => null
    hasErrorCode
    function

    Checks whether an error has a code. The error may also be the code itself.

    Signature
    hasErrorCode(error: unknown, code: string): boolean

    Parameters

    errorunknown

    The error to check, of any shape.

    codestring

    The code to look for.

    Returns

    boolean

    Whether the error has the code.

    Example
    import { Exception, hasErrorCode } from 'jg-exception';
    
    hasErrorCode(new Exception('Not found', { code: 'NOT_FOUND' }), 'NOT_FOUND'); // => true
    hasErrorCode('NOT_FOUND', 'NOT_FOUND'); // => true
    hasErrorCodePrefix
    function

    Checks whether an error's code, or the error itself when it's a string, starts with a prefix.

    Signature
    hasErrorCodePrefix(error: unknown, prefix: string): boolean

    Parameters

    errorunknown

    The error to check, of any shape.

    prefixstring

    The non-empty prefix to look for.

    Returns

    boolean

    Whether the code starts with the prefix; always false for an empty prefix.

    Example
    import { Exception, hasErrorCodePrefix } from 'jg-exception';
    
    hasErrorCodePrefix(new Exception('Denied', { code: 'AUTH_EXPIRED' }), 'AUTH_'); // => true
    isDefaultErrorMessage
    function

    Checks whether a value is, or contains, the default message the package falls back to.

    Signature
    isDefaultErrorMessage(value: string, fullMatch?: boolean): value is string

    Parameters

    valuestring

    The value to check.

    fullMatch?boolean

    Whether the whole value must be the default message.

    Defaults to false.

    Returns

    value is string

    Whether the value matches the default message.

    Example
    import { extractMessage, isDefaultErrorMessage } from 'jg-exception';
    
    isDefaultErrorMessage(extractMessage(undefined), true); // => true
    isException
    function

    Checks whether a value is an Exception, including one created by another installed copy of the package, where instanceof fails.

    Signature
    isException(value: unknown): value is Exception

    Parameters

    valueunknown

    The value to check.

    Returns

    value is Exception

    Whether the value is an Exception.

    Example
    import { Exception, isException } from 'jg-exception';
    
    isException(new Exception('Not found', { code: 'NOT_FOUND' })); // => true
    isException(new Error('Not found')); // => false
    redactFields
    function

    Copies a value, such as a request body headed for an error report, with the values of the named fields replaced by [redacted], however deep they are. Keys match case-insensitively, so password also redacts Password. Plain objects and arrays are copied, and anything else, such as a Date, is kept as it is; the original is never changed.

    Signature
    redactFields(value: unknown, keys: readonly string[]): unknown

    Parameters

    valueunknown

    The value.

    keysreadonly string[]

    The names of the fields to redact, such as ['password', 'token'].

    Returns

    unknown

    The copy, typed unknown because any field may now hold [redacted] instead of its own type.

    Example
    import { redactFields } from 'jg-exception';
    
    const body = { email: '[email protected]', profile: { Password: 'hunter2' } };
    
    redactFields(body, ['password']); // => { email: '[email protected]', profile: { Password: '[redacted]' } }

    Classesjg-exception

    Exception
    class

    An Error with a stable string code and optional JSON data, the error every jg package throws. An app's throw sites can also say how serious the failure is with severity, and ask for it to be logged with shouldLog, for its reporter to read. Neither is serialized.

    Members

    • codeproperty
      string

      the stable code callers handle the error by

    • dataproperty
      IJsonValue | undefined

      context that helps whoever handles or debugs the error

    • severityproperty
      "expected" | "critical"

      how serious the failure is, only when the throw site gave a known severity

    • shouldLogproperty
      boolean

      whether the error is worth logging in full, only when the throw site asked for it

    • constructorconstructor
      new Exception(message: string, options: IExceptionOptions)

      Creates an exception.

    • toJSONmethod
      toJSON(): IExceptionJson

      Converts the exception to a plain object, which JSON.stringify calls.

    • toStringmethod
      toString(): string

      Describes the exception in one line, with its code.

    Example 1
    import { Exception } from 'jg-exception';
    
    const exception = new Exception('The user was not found.', {
      code: 'USER_NOT_FOUND',
      data: { id: 1 },
    });
    exception.code; // => 'USER_NOT_FOUND'
    exception.data; // => { id: 1 }
    Example 2
    import { Exception } from 'jg-exception';
    
    // the message says what failed at this step, and the cause keeps the error underneath
    const readSettings = (text: string): unknown => {
      try {
        return JSON.parse(text);
      } catch (error) {
        throw new Exception("Couldn't read the settings.", { code: 'SETTINGS_INVALID', cause: error });
      }
    };
    readSettings('{"theme":"dark"}'); // => { theme: 'dark' }
    Example 3
    import { Exception, extractMessage } from 'jg-exception';
    
    // the values in `redact` are replaced throughout the exception, including in its causes
    const exception = new Exception("Couldn't verify the token.", {
      code: 'TOKEN_INVALID',
      cause: new Error('Invalid token abc123'),
      redact: ['abc123'],
    });
    extractMessage(exception); // => "Couldn't verify the token.; [CAUSE]: Invalid token [redacted]"
    Example 4
    import { Exception } from 'jg-exception';
    
    // an app's throw site says how its reporter treats the failure, without changing the JSON
    const exception = new Exception('The user was not found.', {
      code: 'USER_NOT_FOUND',
      severity: 'expected',
    });
    exception.severity; // => 'expected'
    exception.shouldLog; // => undefined
    JSON.stringify(exception).includes('severity'); // => false

    Constantsjg-exception

    DEFAULT_MESSAGE
    const

    the message used when none can be extracted from an error

    Declaration
    const DEFAULT_MESSAGE: "The error message could not be extracted, check the logs for more information."
    EXCEPTION_SEVERITIES
    const

    the severities an Exception accepts, from the least to the most serious

    Declaration
    const EXCEPTION_SEVERITIES: readonly ["expected", "critical"]
    REDACTED_VALUE
    const

    the marker that replaces each redacted value, in messages, data, causes, and fields

    Declaration
    const REDACTED_VALUE: "[redacted]"
    UNKNOWN_ERROR_CODE
    const

    the code given to an error that has none, such as when it's sent in an API envelope

    Declaration
    const UNKNOWN_ERROR_CODE: "UNKNOWN_ERROR"

    Typesjg-exception

    IErrorFeedback
    type

    what a UI shows for an error

    Declaration
    type IErrorFeedback = {
        message: string;
        code: string | null;
        isCancelled: boolean;
        isAccessDenied: boolean;
    };

    Members

    • messageproperty
      string

      the copy for the error's code, or the fallback

    • codeproperty
      string | null

      the error's code when it's safe to show, otherwise null

    • isCancelledproperty
      boolean

      whether the error is an AbortError, which usually means the user cancelled the work

    • isAccessDeniedproperty
      boolean

      whether the error's code is one of the access-denied codes

    IErrorFeedbackOptions
    type

    how createErrorFeedback turns errors into what users read

    Declaration
    type IErrorFeedbackOptions = {
        fallbackMessage: string;
        messagesByCode?: Readonly<Record<string, string>> | undefined;
        accessDeniedCodes?: Iterable<string> | undefined;
        publicCodePattern?: RegExp | undefined;
    };

    Members

    • fallbackMessageproperty
      string

      what users read for an error without copy of its own

    • messagesByCodeproperty
      Readonly<Record<string, string>>

      the copy users read for each code, written for them rather than for the logs

    • accessDeniedCodesproperty
      Iterable<string>

      the codes that mean the user isn't allowed to do what they tried, such as a revoked session

    • publicCodePatternproperty
      RegExp

      the codes that are safe to show users without copy of their own, such as /^AUTH_/

    IErrorFeedbackOverrides
    type

    what one call replaces, for a screen whose errors need copy of their own

    Declaration
    type IErrorFeedbackOverrides = {
        fallbackMessage?: string | undefined;
        messagesByCode?: Readonly<Record<string, string>> | undefined;
    };

    Members

    • fallbackMessageproperty
      string

      what users read for an error without copy of its own, instead of the configured fallback

    • messagesByCodeproperty
      Readonly<Record<string, string>>

      the copy users read for each code, which wins over the configured copy for the same code

    IExceptionJson
    type

    an Exception as toJSON returns it, ready to log or send

    Declaration
    type IExceptionJson = {
        name: string;
        code: string;
        message: string;
        data: IJsonValue;
    };

    Members

    • nameproperty
      string
    • codeproperty
      string
    • messageproperty
      string
    • dataproperty
      IJsonValue
    IExceptionOptions
    type

    the options an Exception is created with

    Declaration
    type IExceptionOptions = {
        code: string;
        data?: IJsonValue | undefined;
        cause?: unknown;
        redact?: readonly string[] | undefined;
        severity?: IExceptionSeverity | undefined;
        shouldLog?: boolean | undefined;
    };

    Members

    • codeproperty
      string

      the stable code callers handle the error by, such as USER_NOT_FOUND

    • dataproperty
      IJsonValue

      context that helps whoever handles or debugs the error, such as the id that wasn't found

    • causeproperty
      unknown

      the error underneath, kept as the native cause

    • redactproperty
      readonly string[]

      values to replace with [redacted], as written and URL-encoded, such as a token the cause may hold. They're redacted from the message, the data, and every cause, which becomes a copy that keeps its name, code, and plain fields without running the cause's own code. Nothing keeps them

    • severityproperty
      IExceptionSeverity

      how serious the failure is, set where an app throws it. Packages never set it, and it's never serialized

    • shouldLogproperty
      boolean

      whether the error is worth writing to the logs in full, such as one too large for the error tracker's storage, set where an app throws it. Packages never set it, and it's never serialized

    IExceptionSeverity
    type

    How serious a failure is, for whoever reports it: expected for one the app anticipates, such as a resource that doesn't exist, and critical for one someone must hear about at once. Leaving it out means an ordinary unexpected failure.

    Declaration
    type IExceptionSeverity = (typeof EXCEPTION_SEVERITIES)[number];
    IJsonValue
    type

    A value that survives a trip through JSON. An object's values may also be undefined, because JSON.stringify drops them, so an object with optional fields fits. Interfaces and classes don't fit, because they have no index signature: pass an interface-typed value as { ...value }, or declare its type with type.

    Declaration
    type IJsonValue = string | number | boolean | null | IJsonValue[] | {
        [key: string]: IJsonValue | undefined;
    };

    jg-exception/testing

    Functionsjg-exception/testing

    expectToRejectCode
    function

    Asserts that a promise rejects with an error containing the expected code and optional message text. Message checks use substring matching. It works with any test runner, because it throws a plain Error that the runner reports.

    Signature
    expectToRejectCode(
      promise: PromiseLike<unknown>,
      expectedCode: string,
      expectedMessage?: string,
    ): Promise<void>

    Parameters

    promisePromiseLike<unknown>

    The promise expected to reject.

    expectedCodestring

    The expected error code.

    expectedMessage?string

    Optional text that the extracted rejection message must contain.

    Returns

    Promise<void>

    A promise that resolves with undefined when the rejection matches.

    Throws

    • ErrorExpected the promise to reject with error code "{expectedCode}", but it resolved.
    • ErrorExpected error code "{expectedCode}", but the error did not include a code.
    • ErrorExpected error code "{expectedCode}", but received error code "{actualCode}".
    • ErrorExpected error message to include "{expectedMessage}", but received error message "{actualMessage}".
    Example
    import { expectToRejectCode } from 'jg-exception/testing';
    
    await expectToRejectCode(
      Promise.reject(Object.assign(new Error('Access denied'), { code: 'ACCESS_DENIED' })),
      'ACCESS_DENIED',
      'Access denied',
    );
    expectToThrowCode
    function

    Asserts that a synchronous operation throws an error with the expected code and optional message text. Message checks use substring matching. It works with any test runner, because it throws a plain Error that the runner reports.

    Signature
    expectToThrowCode(
      operation: () => unknown,
      expectedCode: string,
      expectedMessage?: string,
    ): void

    Parameters

    operation() => unknown

    The synchronous operation expected to throw.

    expectedCodestring

    The expected error code.

    expectedMessage?string

    Optional text that the extracted error message must contain.

    Throws

    • ErrorExpected the operation to throw error code "{expectedCode}", but it did not throw.
    • ErrorExpected error code "{expectedCode}", but the error did not include a code.
    • ErrorExpected error code "{expectedCode}", but received error code "{actualCode}".
    • ErrorExpected error message to include "{expectedMessage}", but received error message "{actualMessage}".
    Example
    import { expectToThrowCode } from 'jg-exception/testing';
    
    expectToThrowCode(
      () => {
        throw Object.assign(new Error('Access denied'), { code: 'ACCESS_DENIED' });
      },
      'ACCESS_DENIED',
      'Access denied',
    );