01 · Problem
What is it for?
JavaScript allows throwing any value, including values whose string conversion itself throws.
02 · Mental model
How should you think about it?
An error boundary should always produce a useful representation and preserve causes without recursive failure.
Captures failures and safely normalizes hostile, cyclic or asynchronous thrown values. The compatibility result format still makes an Error returned as ordinary data ambiguous. A tagged Result is a possible future major API.
03 · Control flow
What happens in which order?
- Execute a callback inside an error boundary.
- Await asynchronous outcomes when applicable.
- Preserve successful values.
- Normalize a thrown value safely.
- Aggregate errors without losing context.
04 · Building blocks
The important contracts
NormalizedError / toNormalizedErrorRepresent unknown thrown values safely.noThrow / isError / aggregateErrorsCapture async or sync failures, recognize errors and combine messages.05 · Build it yourself
Implement a small core
Start with this small independent implementation, then use the steps below to make its contract explicit.
- Execute a callback inside an error boundary.
- Await asynchronous outcomes when applicable.
- Preserve successful values.
- Normalize a thrown value safely.
- Aggregate errors without losing context.
type Result<T> =
| { ok: true; value: T }
| { ok: false; error: { name: string; message: string; stack?: string } };
async function capture<T>(run: () => T | PromiseLike<T>): Promise<Result<T>> {
try { return { ok: true, value: await run() }; }
catch (value) {
const error = value instanceof Error ? value : new Error(String(value));
return { ok: false, error: { name: error.name, message: error.message, stack: error.stack } };
}
}06 · Verify
What should you test?
- Test rejected thenables and native promises.
- Normalize cyclic objects and hostile toString values.
- Preserve causes and valid falsy return values.
Try it yourself
Turn the first verification claim into a failing test, implement the smallest fix, then add a boundary case.
Test rejected thenables and native promises.
Compare your approach
Your check should fail when the stated invariant is deliberately broken, and pass again after the fix. Keep setup deterministic and assert an observable result.
07 · Limits
Trade-offs and failure modes
- Returning an Error as ordinary data is ambiguous with the current result API.
- Stack traces are not universally available.
- Normalization must itself remain safe.
The compatibility result format still makes an Error returned as ordinary data ambiguous. A tagged Result is a possible future major API.
08 · Keep exploring
Source code and related ideas
yarn tsx packages/ts-error/examples/basic.ts