Home/Docs/result
RE

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/result
user.service.ts
import { 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.

compose.ts
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.

safe-parse.ts
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.

controller.ts
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#

PropTypeDescription
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 })UDeclarative branching: calls the matching handler and returns its value.
isOk(r)booleanType guard: narrows r to Ok<T>.
isFail(r)booleanType guard: narrows r to Fail<E>.

Examples#

From basic to production-grade — copy and adapt.

user.service.ts
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/resultneverthrowts-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 downloadsGrowing~500k~100k

✅ Supported  ·  ❌ Not supported  ·  ⚠️ Partial / workaround needed. Download counts are approximate weekly npm averages.