<!-- Source: README.md -->

# @codenhub/error

Typed error normalization, result helpers, and opt-in registry presets for
TypeScript applications.

## Installation

```sh
pnpm add @codenhub/error
```

## Usage

The default global registry starts empty. Register application mappings during
initialization, then normalize unknown values into `AppError` instances.

```ts
import { createAppError, getErrorRegistry } from "@codenhub/error";

getErrorRegistry().codes.add("invalid_credentials", {
  message: "Invalid email or password.",
  source: "my-app.auth",
});

const error = createAppError({ code: "invalid_credentials" });
console.log(error.type, error.message);
```

Unmatched values become `type: "unknown"` and use
`"An unexpected error occurred."` unless `fallbackMessage` is provided.

## Documentation

- [Documentation overview](docs/index.md)
- [Error normalization and registries](docs/error-normalization.md)
- [Result helpers](docs/results.md)

## Requirements

- Node.js 22 or newer, or an ES2022-compatible browser, worker, or edge runtime.
- Native `Error` cause support.
- ESM-aware package resolution.
- No runtime dependencies.

Runtime code does not access browser or Node.js globals, making it suitable for
browser, Node.js, SSR, worker, and edge environments that meet these
requirements.

## Notes

- Registry presets are opt-in and do not mutate the global registry on import.
- Configure the mutable global registry during application initialization.
- Registry bucket contents are mutable, but bucket references cannot be replaced.
- Batch registration and registry merging are atomic: invalid input leaves the target unchanged.
- `AppError` instances and read-only registry snapshots are frozen.
- Raw strings passed to `err()` bypass registry matching and use the generic fallback unless `fallbackMessage` is explicit.
- Default JSON serialization includes normalized fields such as `message`, `type`, and `source`, while omitting diagnostic `cause` and `originalError` values.
- `isAppError` recognizes errors created by the current package runtime, not structurally similar values.
- Built-in preset `messageKey` values are stable integration keys for consumer-owned translations; the package does not yet ship a translation map.

### Migration From 0.1

Version 0.2 is a clean API reset. `AppError` values are frozen, `maxDepth` only
accepts integers from `0` through `3`, code and name punctuation is significant,
and `err(string)` no longer exposes that string as its message.

## License

Licensed under Apache-2.0.

---

<!-- Source: docs/index.md -->


# Normalize application errors

The `@codenhub/error` package normalizes unknown failures into a typed
`AppError`, provides result-style control flow, and supplies opt-in browser and
Supabase mappings. It is useful at application boundaries where thrown or
rejected values need a consistent shape before they reach logging, UI, or
recovery logic.

## Setup

### Installation

```sh
pnpm add @codenhub/error
```

### Quick start

The global registry starts empty. Configure it during application
initialization, then normalize unknown values wherever they enter the
application:

```ts
import { createAppError, getErrorRegistry } from "@codenhub/error";

getErrorRegistry().codes.add("invalid_credentials", {
  message: "Invalid email or password.",
  source: "my-app.auth",
});

const error = createAppError({ code: "invalid_credentials" });
```

Applications decide which errors are known by registering mappings or merging
presets. Browser and Supabase presets are opt-in, and importing one does not
mutate the global registry.

### Configuration

Configure the mutable global registry during application initialization, or
create an isolated registry and pass it to `createAppError`. Exact codes, names,
messages, prefixes, and regular-expression patterns can provide normalized
feedback. See [error normalization and registries](docs/error-normalization.md) for
matching priority and preset entrypoints.

## Requirements

- Node.js 22 or newer, or an ES2022-compatible browser, worker, or edge runtime, is required.
- Native `Error` cause support is required.
- ESM-aware package resolution is required.
- The package has no runtime dependencies.

Runtime code does not access browser or Node.js globals. Browser preset imports
do not access DOM globals. Supabase preset imports do not contact services or
require Supabase packages.

## Next steps

- [Error normalization and registries](docs/error-normalization.md) explains
  classification, registry configuration, presets, matching priority,
  defaults, and failure behavior.
- [Result helpers](docs/results.md) covers `Result` construction, transformation,
  matching, and unwrapping for code that prefers explicit success and failure
  values.

---

<!-- Source: docs/error-normalization.md -->


# Error Normalization And Registries

## Normalize Unknown Values

`createAppError(error, options?)` returns an `AppError`. It traverses the input
and wrapper fields `cause`, `originalError`, `error`, `err`, `inner`, and
`innerError` to find classifications.

```ts
import { createAppError } from "@codenhub/error";

const error = createAppError(new Error("Request failed"), {
  fallbackMessage: "Please try again.",
  maxDepth: 2,
});
```

`AppErrorOptions` supports:

| Option            | Default                     | Behavior                                                        |
| ----------------- | --------------------------- | --------------------------------------------------------------- |
| `fallbackMessage` | `DEFAULT_APP_ERROR_MESSAGE` | Message for an unmatched value.                                 |
| `registry`        | `getErrorRegistry()`        | Classification source.                                          |
| `maxDepth`        | `3`                         | Maximum wrapper depth; must be an integer from `0` through `3`. |

`DEFAULT_APP_ERROR_MESSAGE` is `"An unexpected error occurred."`.
`isAppError(value)` identifies errors created by the current package runtime.
Structurally similar or serialized values are not accepted. Passing an `AppError`
to `createAppError` returns the same object when no custom options are supplied;
custom options cause it to be normalized again.

An `AppError` is frozen, implements `Error`, and exposes:

- `type: AppErrorType`, where deterministic matches are `"known"`, pattern
  matches are `"unexpected"`, and unmatched values are `"unknown"`.
- `message`, plus nullable `messageKey` and `source: AppErrorSource` metadata.
- `originalError`, preserving the original top-level input as a non-enumerable diagnostic value.
- `isRetryable`, which defaults to `false` unless matched feedback sets it.

Normalization does not throw for ordinary unknown input, including objects or
proxies whose inspected properties throw. JSON serialization includes `name`,
`message`, `type`, `messageKey`, `source`, and `isRetryable`. It excludes the raw
`cause` and `originalError` diagnostic values, preventing sensitive fields and
cyclic wrapper objects from being serialized through the normalized error.
Registry configuration errors throw `TypeError` at their configuration
boundary. A `maxDepth` outside the integer range from `0` through `3` also
throws `TypeError` before traversal.

## Configure A Registry

`getErrorRegistry()` returns the active mutable global `ErrorRegistry`.
`setErrorRegistry(registry)` replaces it and throws `TypeError` when the value
does not implement the mutable registry interface. `createErrorRegistry(presets?)`
creates an isolated, empty registry and merges optional presets in order.

```ts
import { createAppError, createErrorRegistry } from "@codenhub/error";

const registry = createErrorRegistry();
registry.codes.add("E_RATE_LIMIT", {
  message: "Try again later.",
  messageKey: "error.my-app.api.rateLimit",
  source: "my-app.api",
  isRetryable: true,
});

const error = createAppError({ code: "E_RATE_LIMIT" }, { registry });
```

`ErrorFeedback` requires a non-empty `message` and optionally accepts
`messageKey`, `source`, and `isRetryable`. A `messageKey` must be a dot-separated
key under the `error` namespace with lower-camel-case segments, such as
`error.myApp.api.rateLimit`. A `source` must use dot-separated lowercase
kebab-case segments, such as `my-app.api`. Invalid formats throw `TypeError`.

An `ErrorRegistry` contains exact `codes`, `names`, and `messages` buckets, plus
`prefixes` and regex `patterns`. It also provides `clear()` and `merge()`.
Bucket contents are mutable, but the bucket references are read-only and cannot
be replaced.
Exact buckets implement `add`, `addList`, `get`, `delete`, `clear`, and
`values`. Prefix and pattern buckets omit `get`; their `values()` methods return
`ErrorPrefixDefinition` and `ErrorPatternDefinition` values. All returned
feedback, definitions, and collection arrays are defensively copied.

Code and name identifiers are trimmed but otherwise exact, so punctuation
remains significant. Message and prefix identifiers are trimmed and trailing
`.`, `!`, and `?` are removed. Adding or deleting empty identifiers, adding
inaccessible or invalid feedback fields, and adding or deleting non-`RegExp`
patterns throw `TypeError`; exact-bucket `get` returns `undefined` for an empty
or non-string identifier. Feedback fields are read once and copied into plain
data. Duplicate exact identifiers, prefixes, or equivalent regexes are
replaced. Global and sticky flags are removed from registered regexes.

`addList` validates the complete batch before adding entries. `merge` stages and
validates the complete source before changing its target. Either operation
leaves its target unchanged when validation fails.

Classification priority is:

1. Existing known `AppError` or code, name, exact-message, or prefix match.
2. Existing unexpected `AppError` or regex pattern match.
3. Any remaining `AppError`.
4. An unknown error using the fallback message.

The longest matching normalized prefix wins, including for custom registry
implementations whose prefix definitions are not ordered. Pattern insertion
order determines the first heuristic match. `AppErrorType`, `AppErrorSource`,
`ErrorRegistryBucket`, `ErrorPrefixRegistryBucket`,
`ErrorPatternRegistryBucket`, `ErrorPrefixDefinition`, and
`ErrorPatternDefinition` are exported for consumers typing registry workflows.

## Read-Only Presets

`freezeRegistry(registry)` returns an immutable `ReadonlyErrorRegistry` snapshot.
Its frozen bucket facades expose only read methods at runtime; mutation methods
are absent, including through reflection. Later mutations to the source registry
do not affect the snapshot. Use frozen registries as presets passed to
`createErrorRegistry` or `merge`.

```ts
import { getErrorRegistry } from "@codenhub/error";
import { browserErrorRegistry } from "@codenhub/error/registries/browser";

getErrorRegistry().merge(browserErrorRegistry);
```

Public preset exports are:

| Entrypoint                            | Exports                                                                                                                                  |
| ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `@codenhub/error/registries`          | `browserErrorRegistry`, `browserErrorNames`, `browserErrorPatterns`, `supabaseErrorRegistry`, `supabaseErrorCodes`, `supabaseErrorNames` |
| `@codenhub/error/registries/browser`  | `browserErrorRegistry`, `browserErrorNames`, `browserErrorPatterns`                                                                      |
| `@codenhub/error/registries/supabase` | `supabaseErrorRegistry`, `supabaseErrorCodes`, `supabaseErrorNames`                                                                      |

The raw name/code records and browser pattern tuples are read-only exports; the
prebuilt registry values are read-only. Browser mappings cover common
`DOMException` names and network-message patterns. Ambiguous browser fetch and
network-message matches are not marked retryable; connection refusal and DNS
matches are. Supabase mappings cover selected Auth and PostgreSQL codes plus
Edge Function error names. Built-in `messageKey` values are stable integration
keys for consumer-owned translations. The package does not yet ship a canonical
translation map, so consumers must provide translations when using these keys.
Preset coverage is not exhaustive, and message patterns are heuristic.

## Migration From 0.1

Version 0.2 intentionally resets unstable 0.1 behavior. `AppError` values are
frozen, `isAppError` recognizes only values created by the current package
runtime, `originalError` is non-enumerable, frozen registries are isolated
snapshots without runtime mutators, code and name punctuation remains
significant, and invalid `maxDepth` values throw. Pass `fallbackMessage`
explicitly when a string supplied to `err()` is safe for users.

---

<!-- Source: docs/results.md -->


# Result Helpers

`Result<T>` is the union `Ok<T> | Err`. `Ok<T>` contains `{ ok: true, value }`;
`Err` contains `{ ok: false, error: AppError }`.

```ts
import { err, ok, type Result } from "@codenhub/error";

const loadName = (value: string | null): Result<string> =>
  value === null ? err("missing_name", { fallbackMessage: "Name is missing." }) : ok(value);
```

`ok(value)` wraps a success value, and `ok()` creates `Ok<void>`. `err(error,
options?)` normalizes failures through the same pipeline as `createAppError`.
Raw strings are treated as untrusted diagnostic input, bypass registry matching,
and use the generic fallback message. Supply `fallbackMessage` explicitly only
when the text is safe for users.

The remaining helpers operate only on the success branch unless stated:

| Helper                         | Behavior                                                                                                                             |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
| `map(result, mapper)`          | Maps an `Ok` value and returns an existing `Err` unchanged. Mapper exceptions propagate.                                             |
| `mapAsync(result, mapper)`     | Maps an `Ok` value with an async function returning a Promise. Existing `Err` values return unchanged as a resolved Promise.         |
| `andThen(result, mapper)`      | Runs a mapper returning another `Result`, avoiding nested results. Existing `Err` values are unchanged; mapper exceptions propagate. |
| `andThenAsync(result, mapper)` | Runs an async mapper returning `Promise<Result<U>>`. Existing `Err` values return unchanged as a resolved Promise.                   |
| `match(result, callbacks)`     | Calls exactly one of `onOk(value)` or `onErr(error)` and returns its value. Callback exceptions propagate.                           |
| `unwrap(result)`               | Returns an `Ok` value or throws the `AppError` held by `Err`.                                                                        |
| `unwrapOr(result, fallback)`   | Returns an `Ok` value or the supplied fallback without throwing.                                                                     |

```ts
import { andThen, map, match, ok } from "@codenhub/error";

const result = andThen(ok("42"), (text) => ok(Number(text)));
const doubled = map(result, (value) => value * 2);
const output = match(doubled, {
  onOk: String,
  onErr: (error) => error.message,
});
```

These helpers do not catch callback errors. Use `err()` explicitly when thrown
values should be converted into normalized failures.
