Type-safe error handling for TypeScript.

Wrap any function

  • Flexible wrapping — single function, domain scope, or app-wide
  • Type-safe results — tuples or objects
  • Flexible parsing — transform results and errors with full type inference
  • Built in hooks — run side effects automatically
  • Async utils included — retry, timeout, abort, all, allSettled
  • No try/catch clutter — clean, concise call sites
example.ts
// Wrap any function — zero config
const safeParse = safe.wrap(JSON.parse)
const [data, err] = safeParse(rawInput)
// Shared config for a whole domain
const apiSafe = createSafe({
parseError: errorParser,
defaultError: fallbackError,
onError: errorHook,
})
const fetchUser = apiSafe.wrapAsync(fetchUserAsync)
const fetchPosts = apiSafe.wrapAsync(fetchPostsAsync)
// Same config. Full type narrowing.
const [user, userErr] = await fetchUser('123')
if (userErr) return
const [posts, postsErr] = await fetchPosts(user.id)
console.log(user.name, posts.length)
// Prefer objects? One call to switch.
const objSafe = withObjects(apiSafe)
const fetchPostsObj = objSafe.wrapAsync(fetchPostsAsync)
const { ok, data: posts2, error } = await fetchPostsObj('123')

Introduction

Getting started

A TypeScript utility library providing type-safe error handling with the Result pattern. Configure error handling once, then keep every call site clean and minimal — just like calling a normal function.

Installation

Install @cometloop/safe and get up and running in minutes.

Recommended patterns

The best way to use @cometloop/safe — createSafe with wrap and wrapAsync.

Retry & Timeout

Automatic retry with configurable backoff and timeout/abort support.

Types

Full type reference for SafeResult, SafeOk, SafeErr, ok, err, and more.


Why @cometloop/safe?

Error handling in TypeScript is noisy. Between try-catch blocks, manual type narrowing, and repeated boilerplate, the actual intent of your code gets buried. @cometloop/safe fixes this with a simple idea: configure error handling once, then forget about it at the call site.

With createSafe, you define your error type, error mapping, and observability hooks in one place. Then you wrap your functions and call them normally — no inline options, no lambda wrappers, no repeated configuration. The call site looks like a regular function call that happens to return a [value, error] tuple.

  • Clean call sites — wrap your functions once, call them like any other function
  • Type-safe errors — TypeScript knows the exact error shape at every call site
  • No try-catch blocks — flat, composable code that reads top to bottom
  • Explicit error handling — errors are part of the return type, impossible to forget
  • Automatic retries & timeout — built-in retry with backoff and AbortSignal support
  • Object results — prefer { ok, data, error } over tuples? Use withObjects
  • Zero dependencies

Quick example

The best way to use @cometloop/safe is with createSafe. Configure once, then every call site is just a function call:

import { createSafe } from '@cometloop/safe'

// 1. Configure once — error mapping, logging, and hooks live here
const appSafe = createSafe({
  parseError: (e) => ({
    code: e instanceof Error ? e.name : 'UNKNOWN',
    message: e instanceof Error ? e.message : String(e),
  }),
  defaultError: { code: 'UNKNOWN', message: 'An unknown error occurred' },
  onError: (error) => logger.error(error.code, error.message),
})

// 2. Wrap your functions
const safeFetchUser = appSafe.wrapAsync(fetchUser)
const safeJsonParse = appSafe.wrap(JSON.parse)
// 3. Call sites stay clean — just like calling a normal function
const [user, error] = await safeFetchUser(id)

if (error) {
  // error is fully typed — no `unknown`, no manual narrowing
  console.error(error.code, error.message)
  return
}

console.log(user)

No inline error mappers. No extra options. No lambda wrappers cluttering your logic. Just call the function and handle the result.

The standalone API is also available for quick one-off operations:

import { safe } from '@cometloop/safe'

const [data, error] = safe.sync(() => JSON.parse(jsonString))
const [user, error] = await safe.async(() => fetchUser(id))

Next steps