npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

bakutils-catcher

v6.2.0

Published

Utility library for handling errors elegantly with Decorators and Rust-like algebraic types.

Readme

bakutils-catcher

bakutils-catcher is a lightweight, zero-dependency TypeScript library for robust error handling and functional programming patterns inspired by Rust. It ships as an ESM module and works in Node, the browser, and Dynamics 365 / Power Apps (with first-class support for Xrm promise-likes).

  • Algebraic Data TypesResult, Option, and OneOf for expressive, exception-free control flow.
  • Decorators & function wrappers@Catcher, @DefaultCatcher, @AnyErrorCatcher, and their functional counterparts to intercept thrown errors.
  • ResultTry — turn any throwing (sync or async) function into a Result.
  • Thenable-aware — transparently handles native Promises, generic thenables, and Dynamics Xrm.Async.PromiseLike.

The source types contain rich JSDoc with inline examples — hover them in your editor for more.

Table of Contents

Installation

# npm
npm install bakutils-catcher

# yarn
yarn add bakutils-catcher

# pnpm
pnpm add bakutils-catcher

Decorators require the following in your tsconfig.json:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

If you use decorators without providing a handler and rely on metadata, install reflect-metadata. It is intentionally not bundled, so the library stays dependency-free.

Quick Start

import { Ok, Err, Option, Some, None, Result, catcher } from 'bakutils-catcher';

// 1. Result — model success/failure without throwing
function divide(a: number, b: number): Result<number, string> {
  return b === 0 ? Err('cannot divide by zero') : Ok(a / b);
}

divide(10, 2).match({
  Ok: (value) => console.log('Result:', value), // Result: 5
  Err: (msg) => console.error('Failed:', msg),
});

// 2. Option — null-safe values. Option() turns null/undefined into None.
const apiKey: Option<string> = Option(process.env.API_KEY);
console.log(apiKey.unwrapOr('default-key'));

// 3. catcher — wrap a throwing function so it never throws
class ParseError extends Error {}
const safeParse = catcher(
  (raw: string) => {
    const n = Number(raw);
    if (Number.isNaN(n)) throw new ParseError(`not a number: ${raw}`);
    return n;
  },
  ParseError,
  (err) => 0, // fallback when ParseError is thrown
);

safeParse('42'); // 42
safeParse('abc'); // 0

Result

Result<T, E> is either Ok<T> (success) or Err<E> (failure). It is the union Right<T, E> | Left<T, E>.

import { Ok, Err, Result, isResult } from 'bakutils-catcher';

const ok = Ok(42);              // Result<number, never>
const err = Err('boom');        // Result<never, string>
const empty = Ok();             // Ok with an undefined value is valid

isResult(ok);  // true
isResult({});  // false

Methods

| Method | Ok behavior | Err behavior | | --- | --- | --- | | unwrap() | returns the value | throws the error | | unwrapOr(def) | returns the value | returns def | | unwrapOrElse(fn) | returns the value | returns fn(error) | | isOk() / isErr() | true / false | false / true (type guards) | | map(fn) | returns Ok(fn(value)), or Err if fn throws | returns itself unchanged | | flatMap(fn) | returns fn(value) (an inner Result) | returns itself unchanged | | flatMapAsync(fn) | awaits fn(value)Promise<Result> | returns itself unchanged | | toOption() | Some(value) | None | | match({ Ok, Err }) | calls Ok(value) | calls Err(error) |

Note: Ok.map is exception-safe — if the mapping function throws, you get an Err instead of a thrown exception.

Chaining

flatMap lets you sequence operations that each return a Result, short-circuiting on the first Err:

class OrderError extends Error {}
class UserError extends Error {}

const orderService = (id: string): Result<string, OrderError> =>
  id === 'valid' ? Ok('Order details') : Err(new OrderError('invalid order'));

const userService = (id: string): Result<string, UserError> =>
  id === 'valid' ? Ok('User details') : Err(new UserError('invalid user'));

const processOrder = (orderId: string, userId: string) =>
  orderService(orderId).flatMap((order) =>
    userService(userId).map((user) => `Processed: ${order}, ${user}`),
  );

processOrder('valid', 'valid').unwrap(); // "Processed: Order details, User details"
processOrder('valid', 'bad').isErr();    // true (UserError)

For asynchronous pipelines, use flatMapAsync:

const processOrderAsync = async (orderId: string, userId: string) =>
  orderService(orderId).flatMapAsync(async (order) =>
    userService(userId).map((user) => `Processed: ${order}, ${user}`),
  );

await processOrderAsync('valid', 'valid');

Option

Option<T> is either Some<T> (has a value) or None (empty). It is the union SomeType<T> | NoneType<T>.

There are three ways to build one:

import { Some, None, Option } from 'bakutils-catcher';

Some(42);        // SomeType<number>
None;            // the shared NoneType singleton (frozen)

// Some() THROWS if given null or undefined:
Some(null);      // ❌ Error: Some() cannot be called with null or undefined

The Option() wrapper (recommended)

Option(value) is the safe constructor: it returns None for null/undefined and Some otherwise. It also accepts a lazy callback — if the callback returns null/undefined or throws, you get None.

Option(process.env.API_KEY);     // Some(value) or None — no manual ternary needed
Option(null);                    // None
Option(() => JSON.parse(raw));   // None if JSON.parse throws, else Some(parsed)
Option(() => obj?.maybe?.deep);  // None if the chain is undefined

// Namespace helpers are also available:
Option.Some(1);
Option.None;

Methods

| Method | Description | | --- | --- | | unwrap() | Some: the value. None: throws. | | unwrapOrU() | the value, or undefined for None. | | unwrapOr(def) | the value, or def for None. def may be a value or a () => T callback. | | isSome(value?) | type guard. With no arg: true if it has a value. With an arg: true only if the stored value equals it — no unwrap needed. | | isNone() | type guard for the empty case. | | map(fn) | applies fn and re-wraps via Option() — so a result of null/undefined (or a throw) becomes None. | | mapOr(fn, def) | like map, but falls back to Option(def) when the mapped value is empty. def may be a value or callback. | | flatMap(fn) | applies fn that itself returns an Option, flattening one level. | | flatMapAsync(fn) | async variant returning Promise<Option>. | | flatten() | collapses a nested Option<Option<T>> by one level. Some throws if its value is not an Option. | | okOr(err) | converts to Result: SomeOk, NoneErr(err). err may be a value or callback. | | toSome(value) | (None only) produces a fresh Some from value. | | clone() | deep structuredClone of a Some; None returns itself. | | toString() | string representation. | | match({ Some, None }) | pattern match. | | toJSON() | serialization hook — Some serializes to its value, None serializes to null. |

Examples

// Compare without unwrapping
const opt = Some(5);
opt.isSome(5); // true  — avoids `opt.isSome() && opt.unwrap() === 5`
opt.isSome(9); // false

// map short-circuits to None on missing nested data
Some({ value: { nested: undefined } })
  .map((x) => x.value.nested) // None (re-wrapped)
  .isNone();                  // true

// mapOr supplies a default when the mapped value is empty
Some({ value: { nested: undefined as unknown as string } })
  .mapOr((x) => x.value.nested, 'default')
  .unwrap(); // "default"

// unwrapOr / okOr accept a value or a lazy callback
Option<number>(undefined).unwrapOr(() => 4); // 4

// JSON serialization treats None as null
JSON.stringify({ nested: None });      // '{"nested":null}'
JSON.stringify({ nested: Option(1) }); // '{"nested":1}'
import { isOption } from 'bakutils-catcher';
isOption(Some(1)); // true
isOption(42);      // false

Converting between Result and Option

Some(2).okOr('error');   // Ok(2)
None.okOr('error');      // Err('error')

Ok(2).toOption();        // Some(2)
Err('e').toOption();     // None

ResultTry

ResultTry(fn, args?, errorCase?) runs fn and always resolves to a Promise<Result<T, E>>Ok on success, Err on a thrown error or rejected promise. It infers args from fn's parameters and awaits async results automatically.

import { ResultTry } from 'bakutils-catcher';

function add(a: number, b: number) { return a + b; }
const r1 = await ResultTry(add, [10, 20]); // Ok(30)

async function fetchUser(id: number): Promise<User> { /* ... */ }
const r2 = await ResultTry(fetchUser, [42]); // Ok(User) or Err(Error)

errorCase controls the produced error. It can be a fixed error instance or a mapper (originalError, ...args) => E:

class MyError extends Error {}

function subtract(a: number, b: number) {
  if (a < b) throw new Error('a < b');
  return a - b;
}

// Fixed error
await ResultTry(subtract, [5, 10], new MyError('overridden'));

// Mapper that has access to the original error and the call arguments
await ResultTry(
  subtract,
  [5, 10],
  (orig, a, b) => new MyError(`failed a=${a}, b=${b}: ${(orig as Error).message}`),
);

ResultTry also accepts plain thenables and Xrm promise-likes — see below.

Decorators and function wrappers

Every catcher comes in two flavors: a method decorator (PascalCase, e.g. @Catcher) and a function wrapper (camelCase, e.g. catcher) for when you can't use decorators or prefer a functional style.

The handler always receives the same arguments:

type Handler = (err, fnName: string, ctx, ...args) => Return;
//                ↑ thrown error
//                     ↑ name of the wrapped method/function
//                                    ↑ `this` context at call time
//                                         ↑ the original call arguments

The wrapper preserves the original return shape: if the wrapped function returns a Promise, the wrapper returns a Promise; otherwise it stays synchronous.

Renamed in v5: CatchCatcher, DefaultCatchDefaultCatcher.

@Catcher / catcher

Catches only a specific error subclass; anything else is re-thrown.

import { Catcher, catcher } from 'bakutils-catcher';

class DBError extends Error {}

// Decorator
class Repo {
  @Catcher(DBError, (err, fnName) => {
    console.warn(`${fnName} failed:`, err.message);
    return null; // fallback value returned to the caller
  })
  query(): Row[] | null {
    throw new DBError('timeout');
  }
}

// Function wrapper
const safeQuery = catcher(
  (sql: string) => { throw new DBError('timeout'); },
  DBError,
  (err) => null,
);

The handler also receives the context and arguments, which is useful for building a recovery Result:

class FooBar {
  @Catcher<Result<number, MyErr>, DBError, [number]>(
    DBError,
    (err, fnName, ctx, value) => Err({ err, value }),
  )
  load(value: number): Result<number, MyErr> {
    throw new DBError('nope');
  }
}

@DefaultCatcher / defaultCatcher

Catches all Error instances (it is @Catcher(Error, handler)).

import { DefaultCatcher, defaultCatcher } from 'bakutils-catcher';

class DataService {
  @DefaultCatcher((err) => {
    console.error('An error occurred:', err);
    return undefined; // fallback
  })
  async fetchData(url: string): Promise<Data | undefined> {
    throw new Error('network down');
  }
}

const safeJSON = defaultCatcher(
  (txt: string) => JSON.parse(txt),
  () => ({}), // fallback on any error
);
safeJSON('{ bad'); // {}

@AnyErrorCatcher

Catches literally anything thrown — including non-Error values like strings or numbers — with no instanceof check.

import { AnyErrorCatcher } from 'bakutils-catcher';

class Whatever {
  @AnyErrorCatcher(() => 'fallback')
  run(): string {
    throw 'string-error'; // not an Error instance
  }
}

new Whatever().run(); // "fallback"

OneOf

OneOf<LabelMap> is a type-safe tagged union: a value that is exactly one of several labeled variants. Create variants with createOneOf and handle them exhaustively with match.

import { createOneOf, OneOf } from 'bakutils-catcher';

type Shape = {
  Circle: { radius: number };
  Square: { side: number };
  Rectangle: { width: number; height: number };
};

const circle: OneOf<Shape> = createOneOf('Circle', { radius: 5 });

function area(shape: OneOf<Shape>): number {
  return shape.match({
    Circle: ({ radius }) => Math.PI * radius ** 2,
    Square: ({ side }) => side ** 2,
    Rectangle: ({ width, height }) => width * height,
  });
}

area(circle); // 78.539...

Methods

| Method | Description | | --- | --- | | match(handlers) | exhaustive match — every label must be handled. | | matchPartial(handlers, default) | match a subset; unmatched labels fall through to default(value). | | is(label) | type guard that narrows value to the variant's type. | | equals(other) | true if both variants share the same type and value. | | map(fn) | transforms the contained value, keeping the same label. | | toJSON() | serializes to { type, value }. | | OneOfVariant.fromJSON(json) | static — rebuilds a variant from { type, value }. |

// Narrowing with `is`
if (circle.is('Circle')) {
  circle.value.radius; // typed as number
}

// Partial matching
circle.matchPartial(
  { Circle: ({ radius }) => radius },
  () => 0, // default for Square / Rectangle
);

// Serialization round-trip
const json = circle.toJSON();                 // { type: 'Circle', value: { radius: 5 } }
const back = OneOfVariant.fromJSON<Shape, 'Circle'>(json);

Full example

For an end-to-end pipeline that combines Option, Result (flatMap / flatMapAsync), okOr, ResultTry, a @Catcher-decorated service, and a OneOf state machine, see EXAMPLES.md.

Exported types

For consumers who need to annotate explicitly:

  • Result: Result<T, E>, Right<T, E> (Ok), Left<T, E> (Err)
  • Option: Option<T>, SomeType<T>, NoneType<T>, RemoveOption<T>
  • OneOf: OneOf<LabelMap>, OneOfVariant
  • Decorators: Handler, Wrap, ErrCtor
  • ResultTry: ErrorCase
  • Utilities: ValueOrFn<T>, and the BAKUtilsIs* type guards (BAKUtilsIsPromise, BAKUtilsIsThenable, BAKUtilsIsXrmPromiseLike, BAKUtilsIsFunction, BAKUtilsIsXrmError)
  • Type guards: isResult, isOption