# jg-exception

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

- **Version:** `1.5.0`
- **Runtimes:** Universal
- **Install:** `npm install jg-exception`
- **On npm:** [jg-exception](https://www.npmjs.com/package/jg-exception)
- **Source:** [packages/jg-exception](https://github.com/jesusgraterol/jg-packages/tree/main/packages/jg-exception)
- **Recipes:** [jg-exception recipes](https://jg-packages.jesusgraterol.dev/packages/jg-exception/recipes/index.md)

## 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]' }
```

## <a id="api-reference"></a>jg-exception API reference

Every export of `jg-exception@1.5.0`, 23 in all, with its signature, its errors, and an example. It's generated from the package's TSDoc.

### <a id="api-entry-main"></a>`jg-exception`

- Functions: [`createErrorFeedback`](#api-createErrorFeedback), [`extractMessage`](#api-extractMessage), [`extractRedactedMessage`](#api-extractRedactedMessage), [`getErrorCode`](#api-getErrorCode), [`hasErrorCode`](#api-hasErrorCode), [`hasErrorCodePrefix`](#api-hasErrorCodePrefix), [`isDefaultErrorMessage`](#api-isDefaultErrorMessage), [`isException`](#api-isException), [`redactFields`](#api-redactFields)
- Classes: [`Exception`](#api-Exception)
- Constants: [`DEFAULT_MESSAGE`](#api-DEFAULT_MESSAGE), [`EXCEPTION_SEVERITIES`](#api-EXCEPTION_SEVERITIES), [`REDACTED_VALUE`](#api-REDACTED_VALUE), [`UNKNOWN_ERROR_CODE`](#api-UNKNOWN_ERROR_CODE)
- Types: [`IErrorFeedback`](#api-IErrorFeedback), [`IErrorFeedbackOptions`](#api-IErrorFeedbackOptions), [`IErrorFeedbackOverrides`](#api-IErrorFeedbackOverrides), [`IExceptionJson`](#api-IExceptionJson), [`IExceptionOptions`](#api-IExceptionOptions), [`IExceptionSeverity`](#api-IExceptionSeverity), [`IJsonValue`](#api-IJsonValue)

#### <a id="api-functions"></a>Functions in `jg-exception`

##### <a id="api-createErrorFeedback"></a>`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.

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

**Parameters**

- `options` `IErrorFeedbackOptions`: 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**

```ts
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**

```ts
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.'
```

##### <a id="api-extractMessage"></a>`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.

```ts
extractMessage(error: unknown): string
```

**Parameters**

- `error` `unknown`: The error to extract the message from.

**Returns** `string`: The extracted message, or `DEFAULT_MESSAGE`.

**Example**

```ts
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
```

##### <a id="api-extractRedactedMessage"></a>`extractRedactedMessage`

*Function*

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

```ts
extractRedactedMessage(error: unknown, sensitiveValues: string[]): string
```

**Parameters**

- `error` `unknown`: The error to extract the message from.
- `sensitiveValues` `string[]`: The exact values to redact, such as tokens or passwords.

**Returns** `string`: The extracted message with the sensitive values redacted.

**Example**

```ts
import { extractRedactedMessage } from 'jg-exception';

extractRedactedMessage(new Error('Bad token abc123'), ['abc123']); // => 'Bad token [redacted]'
```

##### <a id="api-getErrorCode"></a>`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.

```ts
getErrorCode(error: unknown): string | null
```

**Parameters**

- `error` `unknown`: The error to read, of any shape.

**Returns** `string | null`: The code, or `null` when the error has none.

**Example**

```ts
import { Exception, getErrorCode } from 'jg-exception';

getErrorCode(new Exception('Not found', { code: 'NOT_FOUND' })); // => 'NOT_FOUND'
getErrorCode(new Error('Not found')); // => null
```

##### <a id="api-hasErrorCode"></a>`hasErrorCode`

*Function*

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

```ts
hasErrorCode(error: unknown, code: string): boolean
```

**Parameters**

- `error` `unknown`: The error to check, of any shape.
- `code` `string`: The code to look for.

**Returns** `boolean`: Whether the error has the code.

**Example**

```ts
import { Exception, hasErrorCode } from 'jg-exception';

hasErrorCode(new Exception('Not found', { code: 'NOT_FOUND' }), 'NOT_FOUND'); // => true
hasErrorCode('NOT_FOUND', 'NOT_FOUND'); // => true
```

##### <a id="api-hasErrorCodePrefix"></a>`hasErrorCodePrefix`

*Function*

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

```ts
hasErrorCodePrefix(error: unknown, prefix: string): boolean
```

**Parameters**

- `error` `unknown`: The error to check, of any shape.
- `prefix` `string`: The non-empty prefix to look for.

**Returns** `boolean`: Whether the code starts with the prefix; always `false` for an empty prefix.

**Example**

```ts
import { Exception, hasErrorCodePrefix } from 'jg-exception';

hasErrorCodePrefix(new Exception('Denied', { code: 'AUTH_EXPIRED' }), 'AUTH_'); // => true
```

##### <a id="api-isDefaultErrorMessage"></a>`isDefaultErrorMessage`

*Function*

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

```ts
isDefaultErrorMessage(value: string, fullMatch?: boolean): value is string
```

**Parameters**

- `value` `string`: 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**

```ts
import { extractMessage, isDefaultErrorMessage } from 'jg-exception';

isDefaultErrorMessage(extractMessage(undefined), true); // => true
```

##### <a id="api-isException"></a>`isException`

*Function*

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

```ts
isException(value: unknown): value is Exception
```

**Parameters**

- `value` `unknown`: The value to check.

**Returns** `value is Exception`: Whether the value is an Exception.

**Example**

```ts
import { Exception, isException } from 'jg-exception';

isException(new Exception('Not found', { code: 'NOT_FOUND' })); // => true
isException(new Error('Not found')); // => false
```

##### <a id="api-redactFields"></a>`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.

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

**Parameters**

- `value` `unknown`: The value.
- `keys` `readonly 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**

```ts
import { redactFields } from 'jg-exception';

const body = { email: 'ada@example.com', profile: { Password: 'hunter2' } };

redactFields(body, ['password']); // => { email: 'ada@example.com', profile: { Password: '[redacted]' } }
```

#### <a id="api-classes"></a>Classes in `jg-exception`

##### <a id="api-Exception"></a>`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**

- `code: string` (property): the stable code callers handle the error by
- `data: IJsonValue | undefined` (property): context that helps whoever handles or debugs the error
- `severity: "expected" | "critical"` (property): how serious the failure is, only when the throw site gave a known severity
- `shouldLog: boolean` (property): whether the error is worth logging in full, only when the throw site asked for it
- `new Exception(message: string, options: IExceptionOptions)` (constructor): Creates an exception.
- `toJSON(): IExceptionJson` (method): Converts the exception to a plain object, which `JSON.stringify` calls.
- `toString(): string` (method): Describes the exception in one line, with its code.

**Example 1**

```ts
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**

```ts
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**

```ts
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**

```ts
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
```

#### <a id="api-constants"></a>Constants in `jg-exception`

##### <a id="api-DEFAULT_MESSAGE"></a>`DEFAULT_MESSAGE`

*Constant*

the message used when none can be extracted from an error

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

##### <a id="api-EXCEPTION_SEVERITIES"></a>`EXCEPTION_SEVERITIES`

*Constant*

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

```ts
const EXCEPTION_SEVERITIES: readonly ["expected", "critical"]
```

##### <a id="api-REDACTED_VALUE"></a>`REDACTED_VALUE`

*Constant*

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

```ts
const REDACTED_VALUE: "[redacted]"
```

##### <a id="api-UNKNOWN_ERROR_CODE"></a>`UNKNOWN_ERROR_CODE`

*Constant*

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

```ts
const UNKNOWN_ERROR_CODE: "UNKNOWN_ERROR"
```

#### <a id="api-types"></a>Types in `jg-exception`

##### <a id="api-IErrorFeedback"></a>`IErrorFeedback`

*Type*

what a UI shows for an error

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

**Members**

- `message: string` (property): the copy for the error's code, or the fallback
- `code: string | null` (property): the error's code when it's safe to show, otherwise `null`
- `isCancelled: boolean` (property): whether the error is an `AbortError`, which usually means the user cancelled the work
- `isAccessDenied: boolean` (property): whether the error's code is one of the access-denied codes

##### <a id="api-IErrorFeedbackOptions"></a>`IErrorFeedbackOptions`

*Type*

how createErrorFeedback turns errors into what users read

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

**Members**

- `fallbackMessage: string` (property): what users read for an error without copy of its own
- `messagesByCode: Readonly<Record<string, string>>` (property): the copy users read for each code, written for them rather than for the logs
- `accessDeniedCodes: Iterable<string>` (property): the codes that mean the user isn't allowed to do what they tried, such as a revoked session
- `publicCodePattern: RegExp` (property): the codes that are safe to show users without copy of their own, such as `/^AUTH_/`

##### <a id="api-IErrorFeedbackOverrides"></a>`IErrorFeedbackOverrides`

*Type*

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

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

**Members**

- `fallbackMessage: string` (property): what users read for an error without copy of its own, instead of the configured fallback
- `messagesByCode: Readonly<Record<string, string>>` (property): the copy users read for each code, which wins over the configured copy for the same code

##### <a id="api-IExceptionJson"></a>`IExceptionJson`

*Type*

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

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

**Members**

- `name: string` (property)
- `code: string` (property)
- `message: string` (property)
- `data: IJsonValue` (property)

##### <a id="api-IExceptionOptions"></a>`IExceptionOptions`

*Type*

the options an Exception is created with

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

**Members**

- `code: string` (property): the stable code callers handle the error by, such as `USER_NOT_FOUND`
- `data: IJsonValue` (property): context that helps whoever handles or debugs the error, such as the id that wasn't found
- `cause: unknown` (property): the error underneath, kept as the native `cause`
- `redact: readonly string[]` (property): 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
- `severity: IExceptionSeverity` (property): how serious the failure is, set where an app throws it. Packages never set it, and it's never serialized
- `shouldLog: boolean` (property): 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

##### <a id="api-IExceptionSeverity"></a>`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.

```ts
type IExceptionSeverity = (typeof EXCEPTION_SEVERITIES)[number];
```

##### <a id="api-IJsonValue"></a>`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`.

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

### <a id="api-entry-testing"></a>`jg-exception/testing`

- Functions: [`expectToRejectCode`](#api-testing-expectToRejectCode), [`expectToThrowCode`](#api-testing-expectToThrowCode)

#### <a id="api-testing-functions"></a>Functions in `jg-exception/testing`

##### <a id="api-testing-expectToRejectCode"></a>`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.

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

**Parameters**

- `promise` `PromiseLike<unknown>`: The promise expected to reject.
- `expectedCode` `string`: 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**

- Expected the promise to reject with error code "{expectedCode}", but it resolved.
- Expected error code "{expectedCode}", but the error did not include a code.
- Expected error code "{expectedCode}", but received error code "{actualCode}".
- Expected error message to include "{expectedMessage}", but received error message "{actualMessage}".

**Example**

```ts
import { expectToRejectCode } from 'jg-exception/testing';

await expectToRejectCode(
  Promise.reject(Object.assign(new Error('Access denied'), { code: 'ACCESS_DENIED' })),
  'ACCESS_DENIED',
  'Access denied',
);
```

##### <a id="api-testing-expectToThrowCode"></a>`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.

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

**Parameters**

- `operation` `() => unknown`: The synchronous operation expected to throw.
- `expectedCode` `string`: The expected error code.
- `expectedMessage?` `string`: Optional text that the extracted error message must contain.

**Throws**

- Expected the operation to throw error code "{expectedCode}", but it did not throw.
- Expected error code "{expectedCode}", but the error did not include a code.
- Expected error code "{expectedCode}", but received error code "{actualCode}".
- Expected error message to include "{expectedMessage}", but received error message "{actualMessage}".

**Example**

```ts
import { expectToThrowCode } from 'jg-exception/testing';

expectToThrowCode(
  () => {
    throw Object.assign(new Error('Access denied'), { code: 'ACCESS_DENIED' });
  },
  'ACCESS_DENIED',
  'Access denied',
);
```
