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 falsehasErrorCodePrefix(error, 'USER_'); // true for any USER_ codegetErrorCode(error); // 'USER_NOT_FOUND', or null when the error has no codeisException(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
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:
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:
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.
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 hiddengetErrorFeedback(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 ownconst 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.'
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.
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.
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 underneathconst 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 causesconst 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 JSONconst exception = new Exception('The user was not found.', { code: 'USER_NOT_FOUND', severity: 'expected',});exception.severity; // => 'expected'exception.shouldLog; // => undefinedJSON.stringify(exception).includes('severity'); // => false
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
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];
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;};
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.
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.