@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
createAttemptfor custom error coercion - Low‑level utilities
createResultandDEFAULT_COERCE_ERROR
Installation
npm i @ryact-utils/attempt
# or
yarn add @ryact-utils/attemptGlossary
| 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.
Errorinstances pass through unchanged.- Strings become
Error(string). - 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 tounknown, but becomes whatever you supply viacreateAttempt(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
