result
v0.2.1@backendkit-labs/result
Replace try/catch with typed, composable error values.
Overview#
Node.js error handling traditionally relies on throw and try/catch — an approach with three deep problems: the flow becomes non-local (a function can throw from any point), TypeScript cannot express what a function might throw so callers must read source code, and a forgotten catch lets exceptions propagate silently to the top of the stack.
Result<T, E> implements the Result Monad (also called Either): a function that can fail returns an object that explicitly and precisely represents either a success (Ok<T>) or a typed failure (Fail<E>). The compiler forces callers to handle both paths. Errors show up in function signatures, compose with map / flatMap, and TypeScript narrows them automatically inside if-blocks — no casting required.
This package is the semantic base of the entire BackendKit suite. By unifying error handling into a single data type, every other library (http-client, pipeline, circuit-breaker) can communicate without friction and offer a coherent developer experience.
Quick Start#
npm install @backendkit-labs/resultimport { ok, fail, type Result } from '@backendkit-labs/result'; async function getUser(id: string): Promise<Result<User, NotFoundError>> { const user = await db.users.findById(id); if (!user) return fail({ type: 'not_found', id }); return ok(user); } const result = await getUser('usr_123'); if (result.ok) { console.log(result.value.name); // User — TypeScript knows } else { handleError(result.error); // NotFoundError — TypeScript knows }
Core Concepts#
ok() and fail()
ok(value) wraps any value into a successful result. fail(error) wraps any object into a typed failure. Both return a Result<T, E>.
map and flatMap
map(r, fn) transforms the success value if the result is ok, passing the error through unchanged. flatMap(r, fn) is the same but fn itself returns a Result — the nesting is automatically flattened. Each step adds its error type to a discriminated union; the caller handles one switch instead of multiple catch blocks.
const name = await getUser('u1').then(r => map(r, user => user.name.toUpperCase()), ); // Result<string, NotFoundError> const invoice = await getUser('u1').then(r => flatMap(r, user => createInvoice(user)), ); // Result<Invoice, NotFoundError | InvoiceError>
andThen and orElse
andThen is an alias for flatMap — preferred when chaining inline. orElse(r, fn) is the mirror image: it runs fn only when the result is a failure, allowing you to recover or remap errors without unwrapping success values.
run() — safe exception boundary
Wraps a function that may throw (third-party code, JSON.parse, etc.) and converts any thrown exception into a typed Fail. Use it at integration boundaries to keep your internal code exception-free.
import { run } from '@backendkit-labs/result'; // JSON.parse throws — run() catches it and returns Result const result = run(() => JSON.parse(rawInput), (err) => ({ type: 'parse_error' as const, message: String(err), })); // Result<unknown, { type: 'parse_error'; message: string }>
match() — declarative handling
Instead of an if/else, match() accepts an object with ok and err handlers and returns whichever branch applies. Useful when mapping a Result to a response shape without branching logic.
import { match } from '@backendkit-labs/result'; return match(result, { ok: (user) => res.json(user), err: (error) => res.status(404).json({ error: error.type }), });
API Reference#
| Prop | Type | Description |
|---|---|---|
| ok(value) | Ok<T> | Wraps a successful value. |
| fail(error) | Fail<E> | Wraps a typed failure. |
| map(r, fn) | Result<U, E> | Transforms the success value; passes error through unchanged. |
| flatMap(r, fn) | Result<U, E|F> | Like map but fn returns a Result — nesting is flattened. |
| andThen(r, fn) | Result<U, E|F> | Alias for flatMap — preferred for inline chaining. |
| orElse(r, fn) | Result<T, F> | Mirror of flatMap: fn runs only on failure, allowing recovery or error remapping. |
| mapError(r, fn) | Result<T, F> | Transforms the error value; success passes through unchanged. |
| run(fn, mapErr) | Result<T, E> | Catches exceptions from fn and converts them to a typed Fail via mapErr. |
| match(r, { ok, err }) | U | Declarative branching: calls the matching handler and returns its value. |
| isOk(r) | boolean | Type guard: narrows r to Ok<T>. |
| isFail(r) | boolean | Type guard: narrows r to Fail<E>. |
Examples#
From basic to production-grade — copy and adapt.
import { ok, fail, type Result } from '@backendkit-labs/result'; interface User { id: string; name: string; email: string } interface NotFoundError { type: 'not_found'; id: string } async function getUser(id: string): Promise<Result<User, NotFoundError>> { const user = await db.users.findById(id); if (!user) return fail({ type: 'not_found', id }); return ok(user); } const result = await getUser('usr_123'); if (result.ok) { console.log(result.value.name); // TypeScript knows: User } else { console.error(result.error.type); // TypeScript knows: 'not_found' }
⚖️ vs. Alternatives#
Comparing @backendkit-labs/result against the most popular Result/Either libraries in the TypeScript ecosystem.
| Feature | @backendkit-labs/result | neverthrow | ts-results |
|---|---|---|---|
| map / flatMap | ✅ | ✅ | ✅ |
| Async map helpers | ✅ Built-in | ⚠️ ResultAsync class | ❌ Manual |
| Zero runtime deps | ✅ | ✅ | ✅ |
| Full TS inference | ✅ | ✅ | ✅ |
| Native BK integration | ✅ circuit-breaker, pipeline, http-client | ❌ | ❌ |
| NestJS module | ✅ | ❌ | ❌ |
| Weekly downloads | Growing | ~500k | ~100k |
✅ Supported · ❌ Not supported · ⚠️ Partial / workaround needed. Download counts are approximate weekly npm averages.