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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@ryact-utils/attempt

v0.1.2

Published

A feather‑weight, type‑safe wrapper that turns _anything_—functions, promises, or raw values—into a predictable **result tuple** \`[error, data, ok]\`.

Readme

@ryact-utils/attempt

A feather‑weight, type‑safe wrapper that turns anything—functions, promises, or raw values—into a predictable result tuple `[error, data, ok]`.

It ships with:

  • A pre‑configured helper attempt
  • A factory createAttempt for custom error coercion
  • Low‑level utilities createResult and DEFAULT_COERCE_ERROR

Installation

npm i @ryact-utils/attempt
# or
yarn add @ryact-utils/attempt

Glossary

| Term | Meaning | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | AttemptResult | The tuple `[error, data, ok]` returned by any wrapper. It also carries twin properties:`result.error`, `result.data`, `result.ok`. | | Success | `ok === true`, `error === undefined`, and `data` holds the value. | | Failure | `ok === false`, `error` holds the coerced error, and `data === undefined`. |


Quick Example: From nested try / catch hell to clean attempt flow

The old way (try/catch hell)

async function loadDashboard(userId: string) {
	try {
		const user = await fetchUserById(userId);
		try {
			const posts = await fetchPostsForUser(user.id);
			try {
				const comments = await fetchCommentsForPost(posts[0].id);
				console.log({ user, posts, comments });
			} catch (e) {
				console.error('Comments step failed:', e);
			}
		} catch (e) {
			console.error('Posts step failed:', e);
		}
	} catch (e) {
		console.error('User step failed:', e);
	}
}

The attempt way (flat & readable)

// flat-flow.ts
async function loadDashboard(userId: string) {
	const [uErr, user] = await attempt(fetchUser(userId));
	if (uErr) return console.error(uErr);

	const [pErr, posts] = await attempt(fetchPosts(user.id));
	if (pErr) return console.error(pErr);

	const [cErr, comments] = await attempt(fetchComments(posts[0].id));
	if (cErr) return console.error(cErr);

	console.log({ user, posts, comments });
}

With attempt, every operation is wrapped in a single line, making the happy path obvious and the error handling explicit—without ever nesting try / catch blocks.

Improvements:

  • ✅ One level of indentation throughout
  • ✅ Linear sequence—handle each error in place, then move on
  • ✅ Success path reads top-to-bottom like a recipe

API Reference

1. attempt(thing, ...args)

Universal dispatcher.

| Argument | Accepts | Action taken | | --------- | ---------------------------------------------------------------------------- | --------------------------------- | | thing | • A function (sync or async)• A Promise• Any other value | Runs / awaits / wraps accordingly | | ...args | Parameters forwarded when thing is a function | — |

Returns either an AttemptResult or Promise<AttemptResult> depending on whether thing ends up async.

Usage

// Sync function
attempt(Math.sqrt, 9);
attempt(() => Math.sqrt(9));

// Function that returns a promise
await attempt(() => fetch('/api').then((r) => r.json()));
await attempt(someAsyncFn, param1, param2);

// Promise
await attempt(someAsyncFn(param1, param2));
await attempt(Promise.resolve('success'));

// Raw value
attempt(123);

2. attempt.sync(fn, ...args)

Executes a synchronous function in a try / catch.

const res = attempt.sync(parseInt, '42');

// or

const res = attempt.sync(() => parseInt('42'));

3. attempt.async(fn, ...args)

Runs an async function (one that returns a promise) and awaits it.

const res = await attempt.async(readFile, 'config.json');

// or

const res = await attempt.async(() => readFile('config.json'));

4. attempt.promise(promise)

Wraps an existing promise.

const res = await attempt.promise(fetch('/api'));

5. attempt.fn(fn, ...args)

Alias for the “smart” branch used internally by attempt(...).
Useful when you explicitly want “function mode”:

const res = attempt.fn(mightBeAsync, 1, 2, 3);

// or

const res = attempt.fn(() => mightBeAsync(1, 2, 3));

6. attempt.any(valueOrFnOrPromise, ...args)

Identical to the top‑level attempt. Provided for symmetry and readability when you’ve created a custom attempt instance (see below).


7. attempt.create

Uses currying to allow you to create a function for reuse.

const attemptFn = attempt.create((p1, p2, p3) => { ... })

// ... later in code

const res1 = attemptFn(p1, p2, p3) // AttemptResult<TData, TError>
const res2 = attemptFn(p1, p2, p3) // AttemptResult<TData, TError>
const res3 = attemptFn(p1, p2, p3) // AttemptResult<TData, TError>

8. attempt.createSync

Follows same pattern as attempt.create but enforces synchronous execution even when the function is asynchronous

This is useful if you have a function that executes some synchronous logic and then returns a Promise

const myAttemptFn = attempt.createSync((p1, p2) => {
	const result = executeSynchronousLogic(p1, p2);

	if (!result.ok) throw new Error();

	return functionThatReturnsPromise();
});

const res1 = myAttemptFn(p1, p2); // AttemptResult<Promise<TData>, TError>

9. attempt.createAsync

Follows same pattern as attempt.create but ensures that the parameter function is treated as an asynchronous function.

This is useful for functions that are poorly typed, or return any, but actually execute asynchronously

const myAttemptFn = attempt.createAsync(someUntypedFunction); // assume someUntypedFunction: any

const result = await myAttemptFn(...args); // myAttemptFn(): Promise<AttemptResult<unknown, TError>>

10. attempt.builder(coerceError)

Builds a customised attempt helper whose .error slot always conforms to your shape.

| Param | Purpose | | ------------- | ----------------------------------------------------------------------------------- | | coerceError | (x: unknown) → MyErrorType — convert anything thrown into a domain‑specific error |

import { createAttempt } from '@ryact-utils/attempt';

const toAxiosError = (x: unknown) =>
	x && typeof x === 'object' && 'isAxiosError' in x ? x : { message: String(x) };

// reusable custom attempt
export const axiosAttempt = createAttempt(toAxiosError);

const res = await axiosAttempt(axios.get('/users'));

All methods (sync, async, promise, fn, any) are available on the returned instance.


11. DEFAULT_COERCE_ERROR

The built‑in coercion logic used by the default attempt.

  1. Error instances pass through unchanged.
  2. Strings become Error(string).
  3. Everything else becomes Error("Unknown error caught with \"attempt\"", { cause }).

You can re‑use it when composing your own coercer:

import { attempt, DEFAULT_COERCE_ERROR } from '@ryact-utils/attempt';

const attemptPlus = attempt.builder((x) => ({
	original: DEFAULT_COERCE_ERROR(x), // keep stack trace
	timestamp: Date.now(),
}));

10. AttemptResult — Type Signature Breakdown

AttemptResult<TReturn, TError> is a tagged union that captures either a success or a failure in a single, tuple-shaped value. It comes in two variants:

// ✅ SUCCESS
type AttemptSuccess<TReturn> = [
  undefined,   // error slot is always undefined
  TReturn,     // the data you asked for
  true         // explicit success flag
] & {
  error:   undefined;
  data:    TReturn;
  success: true;
};

// ❌ FAILURE
type AttemptFailure<TError> = [
  TError,      // the coerced error
  undefined,   // data is undefined
  false        // explicit failure flag
] & {
  error:   TError;
  data:    undefined;
  success: false;
};

// 📦 Combined
type AttemptResult<TReturn, TError> =
  | AttemptSuccess<TReturn>
  | AttemptFailure<TError>;

Tuple Indices vs. Named Properties

| Position | Named property | Meaning | | -------- | -------------- | -------------------------------------------------------- | | [0] | .error | The error object on failure, or undefined on success. | | [1] | .data | The returned data on success, or undefined on failure. | | [2] | .ok | A boolean you can use to narrow the union in TypeScript in the case where TError or TReturn can be falsy. |

Because each variant hard-codes true or false in the third slot, TypeScript’s control-flow analyzer automatically narrows types inside an if (result.success) block:

const result = await attempt.async(fetchUser, "123");

if (result.ok) {
  // TS knows: result.data is User; result.error is undefined
  console.log(result.data.name);
} else {
  // TS knows: result.error is Error; result.data is undefined
  console.error(result.error.message);
}

Generic Parameters

  • TReturn — the type of the successful value (inferred from your function or promise).
  • TError — the type of the coerced error (defaults to unknown, but becomes whatever you supply via createAttempt(coerceError)).

Why a tuple and properties? The tuple form makes destructuring concise:

const [err, value, ok] = await attempt.promise(fetch("/api"));

while the object properties give self-documenting clarity when that fits your style:

if (!result.ok) return result.error;
return doSomething(result.data);

FAQ

Does it replace exceptions?
No—you can still throw if you prefer. attempt simply gives you the option to treat errors as data when that feels cleaner.

Tree‑shakeable?
Yes. ES modules, no external dependencies.

Browser support?
Any runtime that supports ES2019. For older targets, polyfill or down‑compile.

Size?
< 0.5 kB gzipped.


License

MIT