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

try-tuple

v1.0.0

Published

Go-style error handling for TypeScript. Never write try/catch again.

Readme

🛡️ try-tuple

Go-style error handling for TypeScript. Never write try/catch again.

npm version npm downloads license

✅ Zero dependencies  •  ✅ Tiny (~1KB)  •  ✅ Perfect TypeScript narrowing  •  ✅ Sync + Async


The Problem

  • Try/catch is ugly, verbose, and breaks your code flow:
// 😩 Nested try/catch hell
try {
  const user = await db.findUser(id);
  try {
    const posts = await api.getPosts(user.id);
    try {
      await cache.set(user.id, posts);
    } catch (e) {
      /* ... */
    }
  } catch (e) {
    /* ... */
  }
} catch (e) {
  /* ... */
}

The Solution

  • Go solved this years ago. Now you can do it in TypeScript:
// 😎 Clean, flat, readable
import { safe } from "try-tuple";

const [err, user] = await safe(db.findUser(id));
if (err) return handleError(err);

const [err2, posts] = await safe(api.getPosts(user.id));
if (err2) return handleError(err2);

await safe(cache.set(user.id, posts));
// No nesting. No blocks. Just tuples.

Install

# npm
npm install try-tuple

# pnpm
pnpm add try-tuple

# yarn
yarn add try-tuple

# bun
bun add try-tuple

Usage

Async (Promises)

  • Wrap any promise. Get a tuple back.
import { safe } from "try-tuple";

const [err, user] = await safe(db.findUser(id));

if (err) {
  console.error(err.message);
  return;
}

// TypeScript knows user is defined here ✅
console.log(user.name);

Sync

  • Wrap any function that might throw:
const [err, data] = safe.sync(() => JSON.parse(rawJson));

if (err) {
  console.error("Invalid JSON:", err.message);
  return;
}

console.log(data);

Pipe

  • Chain multiple operations. Stops at the first error:
const [err, result] = await safe.pipe(
  () => db.findUser(id),
  (user) => stripe.charge(user.stripeId, 1000),
  (charge) => db.orders.create({ chargeId: charge.id }),
  (order) => email.send({ subject: `Order ${order.id} confirmed` }),
);

if (err) {
  console.error("Pipeline failed at:", err.message);
  return;
}

console.log("Order completed:", result);
  • Mix sync and async freely:
const [err, result] = await safe.pipe(
  () => readFile("config.json", "utf-8"), // async
  (raw) => JSON.parse(raw), // sync — works too
  (config) => startServer(config), // async
);

All (Parallel)

  • Like Promise.all, but returns a tuple:
const [err, results] = await safe.all([
  db.findUser(1),
  db.findUser(2),
  db.findUser(3),
]);

if (err) return handleError(err);

const [user1, user2, user3] = results;

Race

  • Like Promise.race, but safe:
const [err, fastest] = await safe.race([
  fetch("https://api-us.example.com/data"),
  fetch("https://api-eu.example.com/data"),
]);

if (err) return handleError(err);

console.log("Got response from fastest server:", fastest);

Retry

  • Retries a function with optional delay and exponential backoff:
const [err, data] = await safe.retry(
  () => fetch("https://api.com/data"),
  {
    attempts: 3,
    delay: 1000,
    backoff: true, // 1s → 2s → 4s
    onRetry: (err, attempt) => {
      console.log(`⚠️ Attempt ${attempt} failed: ${err.message}`);
    },
  },
);

if (err) {
  console.error("All 3 attempts failed:", err.message);
  return;
}

console.log("Got data:", data);

Wrap

  • Turn any function into a safe version. Once wrapped, use it everywhere:
// Wrap once
const safeJsonParse = safe.wrap((raw: string) => JSON.parse(raw))

// Use everywhere
const [err, data] = safeJsonParse('{"a":1}') // ✅ [null, {a:1}]
const [err2, data2] = safeJsonParse('invalid') // ✅ [Error, null]

TypeScript Narrowing

  • This is where try-tuple really shines.
  • TypeScript automatically narrows the types after you check for errors:
const [err, user] = await safe(db.findUser(id));

if (err) {
  // ✅ TypeScript knows:
  // err → Error
  // user → null
  console.error(err.message);
  return;
}

// ✅ TypeScript knows:
// err → null
// user → User (not null, not undefined)
console.log(user.name);
  • No type casting. No as. No !. It just works.

API Reference

  • safe(promise) Wraps a promise into a result tuple.
const [err, result] = await safe(somePromise)

Parameter Type Description

promise Promise<T> Any promise

Returns: Promise<[Error, null] \| [null, T]>

safe.sync(fn)

  • Wraps a sync function into a result tuple.
const [err, result] = safe.sync(() => riskyOperation())

Parameter Type

fn () => T Any function that might throw

Returns: [Error, null] \| [null, T]

safe.pipe(...fns)

  • Chains functions sequentially. Each function receives the result of the previous one. Stops at the first error.
const [err, result] = await safe.pipe(() => step1(),
  (prev) => step2(prev),
  (prev) => step3(prev),
)
Parameter Type Description

...fns Function[] Functions to chain (sync or async)

Returns: Promise<[Error, null] \| [null, T]>

safe.all(promises)

  • Like Promise.all but returns a result tuple.
const [err, results] = await safe.all([p1, p2, p3])

Parameter Type Description

promises Promise[] Array of promises

Returns: Promise<[Error, null] \| [null, T[]]>

safe.race(promises)

  • Like Promise.race but returns a result tuple.
const [err, fastest] = await safe.race([p1, p2])

Parameter Type Description

promises Promise[] Array of promises

Returns: Promise<[Error, null] \| [null, T]>

safe.retry(fn, options?)

  • Retries a function with configurable attempts, delay and backoff.
const [err, result] = await safe.retry(fn, {
  attempts: 3,
  delay: 1000,
  backoff: true,
  onRetry: (err, attempt) => {},
})

Option Type Default Description

attempts number 3 Maximum number of attempts

delay number 0 Delay between retries in ms

backoff boolean false Use exponential backoff

onRetry (err, attempt) => void — Called after each failed attempt

Returns: Promise<[Error, null] \| [null, T]>

Backoff example:

delay: 1000, backoff: true

Attempt 1 fails → waits 1000ms
Attempt 2 fails → waits 2000ms
Attempt 3 fails → waits 4000ms
Attempt 4 fails → returns [error, null]

safe.wrap(fn)

  • Wraps any function to always return a result tuple instead of throwing.
const safeFn = safe.wrap(originalFn)
const [err, result] = safeFn(args)

Parameter Type Description

fn Function Any sync or async function

Returns: Wrapped function that returns Result<T> (or Promise<Result<T>> for async)

Real-World Examples

  • Express / Fastify Route
import { safe } from "try-tuple";

app.post("/checkout", async (req, res) => {
  const [err, user] = await safe(db.users.findById(req.userId));

  if (err) return res.status(500).json({ error: "Database error" });
  if (!user) return res.status(404).json({ error: "User not found" });

  const [err2, charge] = await safe(
    stripe.charges.create({
      amount: req.body.amount,
      customer: user.stripeId,
    }),
  );

  if (err2) return res.status(402).json({ error: "Payment failed" });

  const [err3, order] = await safe(
    db.orders.create({
      userId: user.id,
      chargeId: charge.id,
      amount: req.body.amount,
    }),
  );

  if (err3) return res.status(500).json({ error: "Could not create order" });

  res.json({ orderId: order.id, status: "confirmed" });
});
  • File Operations
import { safe } from 'try-tuple'
import { readFile, writeFile } from 'fs/promises'

async function updateConfig(key: string, value: string) {
  const [readErr, raw] = await safe(readFile('config.json', 'utf-8'))

  if (readErr) return console.error('Cannot read config:', readErr.message)

  const [parseErr, config] = safe.sync(() => JSON.parse(raw))

  if (parseErr) return console.error('Invalid JSON:', parseErr.message)

  config[key] = value
  config.updatedAt = new Date().toISOString()

  const [writeErr] = await safe(
    writeFile('config.json', JSON.stringify(config, null, 2))
  )

  if (writeErr) return console.error('Cannot write config:', writeErr.message)

  console.log(`✅ Updated ${key}`)
}

External API with Retry

import { safe } from 'try-tuple'

async function fetchUserData(userId: string) {
  const [err, data] = await safe.retry(async () => {
    const res = await fetch(`https://api.example.com/users/${userId}`)

    if (!res.ok) throw new Error(`HTTP ${res.status}`)

    return res.json()
  }, {
    attempts: 3,
    delay: 500,
    backoff: true,
    onRetry: (err, attempt) => {
    console.log(`⚠️ Attempt ${attempt} failed: ${err.message}`)
  }})

  if (err) {
    console.error('All attempts failed:', err.message)
    return null
  }

  return data
}

Full Pipeline

import { safe } from 'try-tuple'

async function processOrder(userId: string, amount: number) {
  const [err, result] = await safe.pipe(
    () => db.users.findById(userId),
    (user) => stripe.charges.create({ amount, customer: user.stripeId }),
    (charge) => db.orders.create({ userId, chargeId: charge.id, amount }),
    (order) => email.send({
      to: userId,
      subject: `Order ${order.id} confirmed`,
      body: `You were charged $${amount / 100}`,
    }),
  )

  if (err) {
    console.error('Order failed:', err.message)
    await notifyTeam(err)
    return null
  }

  return result
}

Wrapping Libraries

import { safe } from 'try-tuple'
import jwt from 'jsonwebtoken'

// Wrap once
const safeVerify = safe.wrap(
  (token: string) => jwt.verify(token, process.env.JWT_SECRET!)
)

// Use everywhere
app.use(async (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1]
  if (!token) return res.status(401).json({ error: 'No token' })

  const [err, payload] = safeVerify(token)
  if (err) return res.status(401).json({ error: 'Invalid token' })

  req.user = payload
  next()
})

Comparison

  • try/catch vs try-tuple
// ❌ try/catch
let user;
try {
  user = await db.findUser(id);
} catch (err) {
  return handleError(err);
}
// user might still be undefined here 😬

// ✅ try-tuple
const [err, user] = await safe(db.findUser(id));

if (err) return handleError(err);
// user is guaranteed to be defined here 😊
  • Why not just .catch()?
// ❌ .catch() — awkward flow
const user = await db.findUser(id).catch((err) => {
  handleError(err);
  return null;
});

if (!user) return; // is it null because of error or because user doesn't exist?

// ✅ try-tuple — clear distinction
const [err, user] = await safe(db.findUser(id));

if (err) return handleError(err); // error

if (!user) return handleNotFound(); // no user

Error Coercion

  • try-tuple always gives you a proper Error object, even when the thrown value is not an Error:
// throw 'oops' → Error('oops')
// throw 404 → Error('404')
// throw { code: 'FAIL' } → Error('[object Object]')
// throw new Error('ok') → Error('ok') (unchanged)

❓ FAQ

Does it work with any promise?

  • Yes. fetch, database queries, file operations, anything that returns a Promise.

Does it add overhead?

  • Negligible. It's just a try/catch wrapper (~1KB). No runtime magic.

Can I use it with Express/Fastify/Hono?

  • Yes. Works with any framework. It's just a function.

Does it work in the browser?

  • Yes. Zero Node.js dependencies.

CommonJS or ESM?

  • Both. The package ships with .js (CJS) and .mjs (ESM).
// ESM
import { safe } from "try-tuple";

// CJS
const { safe } = require("try-tuple");

📄 License

Published under the MIT license. Made by Wallace Frota