How to use @owlmeans/error — ResilientError, the error class registry, marshalling errors across a service boundary and back, and the i18n namespace error messages resolve through. Auto-invoked when importing from this package, declaring a typed framework error, or normalizing a caught error.
Scanned 9/22/2026
Install to Claude Code
npx -y skills add owlmeans/common --skill error --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Error?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/owlmeans-error-common)More formats (shields.io, HTML) on the badges page.
---
name: error
description: How to use @owlmeans/error — ResilientError, the error class registry, marshalling errors across a service boundary and back, and the i18n namespace error messages resolve through. Auto-invoked when importing from this package, declaring a typed framework error, or normalizing a caught error.
user-invocable: false
---
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
# @owlmeans/error
**Layer:** Core
**Install:** `"@owlmeans/error": "^0.1.18-rc.28"` in `dependencies`
## Key Exports
| Export | Description |
|--------|-------------|
| `ResilientError` | Base class — every framework error extends it |
| `ResilientError.registerErrorClass(Class)` | Register a subclass so it survives a round trip |
| `ResilientError.ensure(err)` | Turn anything caught into a `ResilientError` |
| `ResilientError.marshal(err)` / `err.marshal()` | Flatten into a plain `Error` a transport can carry |
| `enuserError(err)` | `ensure`, typed to the subclass you expect |
| `marshalError(err)` | `ensure` then `marshal`, for a boundary that only sends `Error` |
| `isResilientError(value)` | Structural check that holds across duplicate module copies |
| `SEPARATOR` (`'\|\|\|'`), `RESILENT_ERROR` | The marshalling separator and the base type name |
| `RESILIENT_BRAND`, `CONVERTER_REGISTRY`, `CATCH_ALL_CONVERTER` | The `Symbol.for` keys every copy shares |
| `Converter` | `{ match, convert, isMarshaled, unmarshal }` — one registry entry |
| `ResilientErrorConstructor` | The constructor shape `registerErrorClass` accepts |
| `ValueOrError<T>` | `T \| ResilientError`, for a result that carries either |
## Declaring an error
A subclass owns a static `typeName` and prefixes its messages, so the pair `type` + `message` is
enough to identify what went wrong anywhere the error travels. **Register it** — registration is
what makes a marshaled error come back as the class it was thrown as. Skip it and the far side gets
an unusable `ResilientError` whose `type` is the whole marshaled string (see below).
```typescript
import { ResilientError } from '@owlmeans/error'
export class ApiError extends ResilientError {
public static override typeName = 'ApiError'
constructor(message: string = 'error') {
super(ApiError.typeName, `api:${message}`)
}
}
export class RateLimitError extends ApiError {
public static override typeName = `${ApiError.typeName}:RateLimit`
constructor(message: string = 'error') {
super(`rate-limit:${message}`)
this.type = RateLimitError.typeName
}
}
ResilientError.registerErrorClass(ApiError)
ResilientError.registerErrorClass(RateLimitError)
throw new RateLimitError('per-minute')
```
A subclass of a subclass calls `super` with the message alone and then re-stamps `this.type` — the
parent supplies its own prefix, so the final message reads `api:rate-limit:per-minute`.
**A refusal of the caller's condition declares its HTTP status** as `public static httpStatus = <4xx>`;
a fault — a broken peer, a timeout, a missing configuration, a bug — declares nothing and answers 500.
One principle picks the number:
| Status | The caller's condition |
|---|---|
| 400 | the request is malformed |
| 402 | the balance or plan does not pay for it |
| 404 | the addressed target does not exist, or is another organization's |
| 409 | the target's current state conflicts with the request |
| 422 | the request is understood, and its content or body is refused |
Declare it on the leaf class, never on a family base: a static is inherited, so a base's status
would reach every fault that extends it. A class thrown for several causes takes the status of the
cause a caller can reach, and one whose causes are told apart only by message declares nothing.
Nothing here reads it — `@owlmeans/server-api` does, structurally, and answers 500 for any class
without an integer 4xx declaration (see the `server-api` skill). A permission refusal extends
`AuthForbidden` (403) instead of declaring.
`registerErrorClass` takes a second, native-class argument, and `ensure` takes a second
`throwOnUnknown` argument. **Neither has any effect** — a catch-all converter is pushed onto the
registry when this package loads and it is the first entry `ensure` tests for conversion, so no
later converter and no `throwOnUnknown` branch is ever reached. Register the class alone, and treat
`ensure` as taking one argument.
## Normalizing what you caught
`ensure` gives you a `ResilientError` for anything caught, but only a **registered, marshaled**
error survives with its identity intact. Route errors that must keep their type through
`marshal`/`ensure`; do not rely on `ensure` alone to normalize an arbitrary throw.
```typescript
try { /* ... */ } catch (e) {
const err = ResilientError.ensure(e as Error)
if (err instanceof RateLimitError) { /* the registered class came back */ }
}
```
What `ensure` actually does, in order:
| Input | Result |
|-------|--------|
| A `ResilientError` from any copy of this package | returned untouched |
| A `SyntaxError` | rethrown — never converted |
| An `Error` marshaled from a **registered** class | unmarshaled into that class, `type` and `message` restored |
| Anything else | a bare `ResilientError` whose **`type` is the original `message`** and whose **`message` is the original stack** |
That last row is the trap: the fields are shifted, so an unregistered marshaled error arrives with
`type` set to the whole `Type|||message|||stack` string, and a plain `new Error('boom')` arrives with
`type: 'boom'`. Read `.type` only where the error came back through the registered path; keep the
original around when you need its message.
**`SyntaxError` is never converted — it is rethrown.** A `SyntaxError` in this framework means the
process is wired wrong (an unknown alias, a missing service, a route cycle), and it must crash
rather than reach a user as a handled failure. Do not throw one for a runtime condition a caller is
expected to handle.
## Duplicate module copies behave as one
A process can load this package more than once — `bun --preserve-symlinks` over linked workspaces
keys a module by its unresolved path, so a package that keeps its own
`node_modules/@owlmeans/error` link loads a second copy. Every copy acts as one:
- **One registry.** `ResilientError.converters` is the array kept on `globalThis` under
`Symbol.for('@owlmeans/error:converters')`; every copy registers into it and unmarshals from it,
in registration order, so the **last registration of a type name wins** across copies. One
catch-all converter exists per process.
- **One brand.** Every instance inherits `Symbol.for('@owlmeans/error:resilient')` from its copy's
prototype. `isResilientError(value)` is brand + string `type` + `marshal` function; `ensure`
returns such a value untouched and `marshal` keeps its `type`. A lookalike without the brand is
not a resilient error.
- **`instanceof` across copies.** `ResilientError[Symbol.hasInstance]` answers natively first, then
structurally: the instance's lineage of OWN `typeName`s must end with the checked class's lineage.
A class from another copy matches itself and its ancestors, never a sibling or a parent. A class
that does not declare its own `typeName` matches natively only — declare one on every subclass.
## Crossing a service boundary
`marshal` flattens `type`, `message`, the selected stack exposure and an optional incident id into
one `Error` message joined by `SEPARATOR`. On the far side `ensure` recognises the prefix and rebuilds
the registered class, so a typed error thrown in a backend is caught as the same class in a client.
The HTTP boundary attaches the incident id to the logged error and sends only that id in production;
development callers may select the stack explicitly. Override
`finalizeUnmarshal()` on a subclass that needs to rebuild state from its message after that.
## Messages are i18n keys
Importing this package registers the `errors` translation library for every bundled locale. UIs
resolve an error by its `type` — `errors.<type>`, with a form- or screen-scoped key tried first —
so the message a user reads comes from the translations, never from the thrown string. Ship a
translation for each error type you declare.
## Depends On
- `@owlmeans/i18n` — the translation library the `errors` namespace is registered in
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!