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

catch-chain

v1.1.0

Published

Fluent, composable, type-safe error matching and recovery for promises

Readme

catch-chain

npm version npm downloads bundle size license CI TypeScript zero dependencies

Fluent error matching and recovery for JavaScript promises.

await fetchUser(id)
  .catch(onErr(isNotFound).return(null))
  .catch(onErr(isRateLimited).do(logWarning).throw())
  .catch(onAnyErr.throw(err => new AppError(`Unexpected: ${err.message}`)))

No more catch blocks with nested if/else chains. Define what to match, what to do, and how to recover - in a readable, composable chain.

Install

npm install catch-chain

Why

A typical promise .catch() quickly turns into this:

await fetchUser(id).catch(err => {
  if (err instanceof NotFoundError) {
    return null
  }
  if (err instanceof RateLimitError) {
    logWarning(err)
    throw new RetryableError()
  }
  reportToSentry(err)
  throw new AppError('Unexpected failure')
})

Every handler mixes matching, side effects, and recovery in one block. As cases grow, readability suffers. catch-chain separates these concerns:

await fetchUser(id)
  .catch(onErr(isNotFound).return(null))
  .catch(onErr(isRateLimited).do(logWarning).throw(new RetryableError()))
  .catch(onAnyErr.do(reportToSentry).throw(err => new AppError(`Unexpected: ${err.message}`)))
  • Each .catch() handles one case.
  • Unmatched errors pass through to the next handler - just like multi-catch in Java or pattern matching in Rust.

API

onErr(matcher)

Creates a chain that only activates when matcher returns true for the rejected value.

import { onErr } from 'catch-chain'

const isNotFound: ErrorMatcher<NotFoundError> = 
  (reason): reason is NotFoundError => reason instanceof NotFoundError

await fetchUser(id).catch(onErr(isNotFound).return(null))

A matcher is a type guard - it receives unknown and narrows to a specific error type. This gives you full type safety in .do() and .return()/.throw() callbacks.

onAnyErr

A pre-built chain that matches any Error instance. Shorthand for onErr(reason => reason instanceof Error).

import { onAnyErr } from 'catch-chain'

await riskyOperation().catch(onAnyErr.return('fallback'))

Non-Error rejections (strings, objects, etc.) pass through unhandled.

.do(effect)

[Pipe operation] Run a side effect when the error matches. Returns the chain for further chaining. The effect can be sync or async.

// Sync effect - log and recover
.catch(onAnyErr
  .do(err => console.error(err))
  .return(defaults))
  
// Async effect
.catch(onAnyErr
  .do(async err => await alertOncall(err))
  .throw(new ServiceUnavailableError()))

// Multiple effects, executed in order
.catch(onAnyErr
  .do(err => metrics.increment('errors'))
  .do(logger.error)
  .throw())

.return(value)

[Terminal operation] Recover from the error by resolving the promise with value. If the error doesn't match, it's re-thrown unchanged.

// Static value
.catch(onErr(isNotFound).return(null))

// Sync mapper - derive a value from the error
.catch(onErr(isNotFound).return(err => defaultItemFor(err.resourceId)))

// Async mapper - derive a value from the error
.catch(onErr(isNotFound).return(async err => await fetchFallback(err.resourceId)))

When value is a function, it's called with the matched error and its return value is used.

.throw(error?)

[Terminal operation] Re-throw the original error, replace it, or map it. If the error doesn't match, the original is re-thrown unchanged.

// No args - re-throw original (useful after side effects)
.catch(onErr(isDatabaseError)
  .do(err => logger.error(err))
  .throw())

// Constant - replace with a different error
.catch(onErr(isDatabaseError)
  .throw(new AppError('Something went wrong')))

// Sync mapper - transform the error
.catch(onErr(isDatabaseError)
  .throw(err => new AppError(`DB failed: ${err.code}`)))

// Async mapper - transform the error
.catch(onErr(isDatabaseError)
  .throw(async err => new AppError(`DB failed: ${await resolveCode(err)}`)))

Effects and producers should not throw

Effects (.do()) and value/error producers (.return() / .throw() callbacks) are expected to not throw or reject. The library does not catch errors from these functions - if one throws, the thrown error replaces the original and propagates to the caller.

Patterns

Multi-catch

Chain multiple .catch() calls - each handles one error type. Unmatched errors fall through.

const user = await fetchUser(id)
  .catch(onErr(isNotFound).return(null))
  .catch(onErr(isTimeout).throw(new RetryableError()))
  .catch(onAnyErr.throw(new AppError('Unexpected')))

Log and re-throw

Use .do() for side effects, then .throw() to re-throw (same or different error).

await processPayment(order)
  .catch(onAnyErr
    .do(err => logger.error('Payment failed', { orderId: order.id, err }))
    .throw(err => new PaymentError(err.message)))

Graceful degradation

Return a default value when a non-critical operation fails.

const preferences = await loadPreferences(userId)
  .catch(onAnyErr.return(DEFAULT_PREFERENCES))

Custom matchers

A matcher is any function with the signature (reason: unknown) => reason is E. Build matchers that check properties, codes, or any condition.

const isConflict: ErrorMatcher<HttpError> =
  (reason): reason is HttpError =>
    reason instanceof HttpError && reason.status === 409

await saveDocument(doc)
  .catch(onErr(isConflict).return(await mergeAndRetry(doc)))

Effect-only handling

Use .do() with .throw() when you want to observe the error without changing the outcome.

await backgroundSync()
  .catch(onAnyErr
    .do(err => telemetry.record('sync_failed', err))
    .throw())

Types

All types are inferred automatically. You don't need to import anything beyond onErr and onAnyErr.

A custom matcher is a type guard with this shape:

type ErrorMatcher<E extends Error> = (reason: unknown) => reason is E

The .do(), .return(), and .throw() callbacks receive the narrowed type from your matcher - no manual annotations needed.

Design

  • Match or pass through - unmatched errors propagate unchanged, enabling composable multi-catch chains
  • Type-safe - matchers are type guards, so .do(), .return(), and .throw() callbacks receive the narrowed error type
  • Zero dependencies - only uses native Error, Promise, and instanceof
  • Tiny - under 1.5 KB, ships ESM + CJS with full TypeScript declarations

License

MIT