Skip to content

Result Types

A Result<T, E> represents either success (ok) or failure (err). It replaces try/catch with explicit typing, giving you compile-time guarantees about error handling.

For async SDK/vendor boundaries, the canonical awaitly pattern is tryAsyncBoundary from awaitly/result/retry.

// fetchUser returns AsyncResult<User, 'NOT_FOUND'>
const result = await fetchUser('123');
if (result.ok) {
console.log(result.value.name);
} else {
// TypeScript knows result.error is 'NOT_FOUND'
console.log(`User ${result.error}`);
}

Results give you:

  • Type safety: TypeScript knows exactly what errors can occur
  • Explicit handling: Errors are part of the return type, not hidden
  • Composability: Results chain together naturally
┌─── The success value type
│ ┌─── The error type
▼ ▼
Result<Value, Error>
Examples:
Result<number, 'DIVIDE_BY_ZERO'> → number on success, 'DIVIDE_BY_ZERO' on failure
Result<User, 'NOT_FOUND'> → User on success, 'NOT_FOUND' on failure
Result<Order, 'INVALID' | 'EXPIRED'> → Order on success, one of two errors on failure
import { ok, err, type AsyncResult } from 'awaitly';
// Synchronous
const divide = (a: number, b: number): Result<number, 'DIVIDE_BY_ZERO'> =>
b === 0 ? err('DIVIDE_BY_ZERO') : ok(a / b);
// Asynchronous
const fetchUser = async (id: string): AsyncResult<User, 'NOT_FOUND'> => {
const user = await db.users.find(id);
return user ? ok(user) : err('NOT_FOUND');
};

Check result.ok to determine success or failure:

const result = divide(10, 2);
if (result.ok) {
console.log(result.value); // 5
} else {
console.log(result.error); // TypeScript knows this is 'DIVIDE_BY_ZERO'
}

After checking result.ok, TypeScript narrows the type:

  • If result.ok is true, you can access result.value
  • If result.ok is false, you can access result.error

Use isOk and isErr for functional-style checks:

import { isOk, isErr } from 'awaitly';
const result = divide(10, 0);
if (isOk(result)) {
console.log(result.value);
}
if (isErr(result)) {
console.log(result.error); // 'DIVIDE_BY_ZERO'
}

Transform the success value, leaving errors unchanged:

import { map } from 'awaitly';
const result = ok(5);
const doubled = map(result, (n) => n * 2);
// { ok: true, value: 10 }

Transform the error, leaving success values unchanged:

import { mapError } from 'awaitly';
const result = err('NOT_FOUND');
const mapped = mapError(result, (e) => ({ type: e, status: 404 }));
// { ok: false, error: { type: 'NOT_FOUND', status: 404 } }

Chain operations that might fail. The function only runs if the previous result was ok:

import { andThen } from 'awaitly';
const result = ok(10);
const chained = andThen(result, (n) =>
n > 0 ? ok(n * 2) : err('NEGATIVE')
);
// { ok: true, value: 20 }
const negative = ok(-5);
const failed = andThen(negative, (n) =>
n > 0 ? ok(n * 2) : err('NEGATIVE')
);
// { ok: false, error: 'NEGATIVE' }

Handle both cases in one expression:

import { match } from 'awaitly';
const result = divide(10, 2);
const message = match(result, {
ok: (value) => `Success: ${value}`,
err: (error) => `Error: ${error}`,
});
// "Success: 5"
import { unwrap } from 'awaitly';
const result = divide(10, 2);
const value = unwrap(result); // 5
const badResult = divide(10, 0);
const boom = unwrap(badResult); // Throws!

Return a default value on error:

import { unwrapOr } from 'awaitly';
const result = divide(10, 0);
const value = unwrapOr(result, 0); // 0

unwrapOrElse - Get Value or Compute Default

Section titled “unwrapOrElse - Get Value or Compute Default”

Compute the default from the error:

import { unwrapOrElse } from 'awaitly';
const result = divide(10, 0);
const value = unwrapOrElse(result, (error) => {
console.log('Failed with:', error);
return 0;
});
// Logs: Failed with: DIVIDE_BY_ZERO
// Returns: 0

Convert exceptions into Results:

import { from } from 'awaitly';
const result = from(() => JSON.parse('{"valid": true}'));
// { ok: true, value: { valid: true } }
const invalid = from(() => JSON.parse('not json'));
// { ok: false, error: SyntaxError }
import { fromPromise } from 'awaitly';
const result = await fromPromise(fetch('/api/data'));
// Success: { ok: true, value: Response }
// Network error: { ok: false, error: TypeError }

Convert exceptions into typed errors:

import { tryAsync } from 'awaitly';
const result = await tryAsync(
() => fetch('/api/data').then(r => r.json()),
(thrown) => ({ type: 'FETCH_FAILED' as const, cause: thrown })
);
// Success: { ok: true, value: { ...data } }
// Failure: { ok: false, error: { type: 'FETCH_FAILED', cause: Error } }

Returns the first error encountered, or all values if everything succeeds:

import { all } from 'awaitly';
const results = [ok(1), ok(2), ok(3)];
const combined = all(results);
// { ok: true, value: [1, 2, 3] }
const withError = [ok(1), err('FAILED'), ok(3)];
const failed = all(withError);
// { ok: false, error: 'FAILED' }

Returns Ok with every value when all succeed, or Err with an array of { error, cause? } entries when any fail:

import { allSettled } from 'awaitly';
const allOk = [ok(1), ok(2), ok(3)];
const settled = allSettled(allOk);
// { ok: true, value: [1, 2, 3] }
const mixed = [ok(1), err('A'), ok(3), err('B')];
const failed = allSettled(mixed);
// {
// ok: false,
// error: [
// { error: 'A' },
// { error: 'B' },
// ]
// }

partition - Separate Successes and Failures

Section titled “partition - Separate Successes and Failures”

Returns a { values, errors } object — never fails:

import { partition } from 'awaitly';
const results = [ok(1), err('A'), ok(3)];
const { values, errors } = partition(results);
// values: [1, 3]
// errors: ['A']

Most operations have async variants for working with promises:

import { allAsync, anyAsync } from 'awaitly';
const results = await allAsync([
fetchUser('1'),
fetchPosts('1'),
fetchComments('1'),
]);
// All succeed: { ok: true, value: [User, Post[], Comment[]] }
// One fails: { ok: false, error: 'NOT_FOUND' }

Unwrap nested Results:

import { flatten, ok, err } from 'awaitly/result';
const nested = ok(ok(42));
const flat = flatten(nested);
// { ok: true, value: 42 }
const nestedErr = ok(err('INNER'));
const flat2 = flatten(nestedErr);
// { ok: false, error: 'INNER' }

Rehydrate Results from JSON, RPC, or server actions with type-safe error handling:

import { deserialize, DESERIALIZATION_ERROR } from 'awaitly/result';
// Valid serialized Result
const result = deserialize<User, AppError>(rpcResponse);
if (result.ok) {
console.log(result.value); // User
}
// Invalid input returns a typed DeserializationError
const invalid = deserialize({ foo: 'bar' });
if (!invalid.ok && invalid.error.type === DESERIALIZATION_ERROR) {
console.log('Bad input:', invalid.error.value);
}

Use object-style config when wrapping SDK/vendor edges so classification and retry policy stay together:

import { tryAsyncBoundary } from 'awaitly/result/retry';
const result = await tryAsyncBoundary({
try: () => paymentProvider.authorize(card, total),
catch: (cause) =>
isTimeoutAfterCapture(cause)
? new PaymentLimbo({ attemptId, cause })
: new TransientVendorError({ vendor: 'stripe', cause }),
retry: {
attempts: 4,
initialDelay: 100,
shouldRetry: (e) => e instanceof TransientVendorError,
},
});
// 1) Timeout-after-capture: classify as do-not-retry
const payment = await tryAsyncBoundary({
try: () => gateway.authorize(card, total),
catch: (cause) =>
isTimeoutAfterCapture(cause)
? new PaymentLimbo({ attemptId, cause })
: new TransientVendorError({ vendor: 'stripe', cause }),
retry: {
attempts: 3,
initialDelay: 100,
backoff: 'fixed',
shouldRetry: (e) => e instanceof TransientVendorError,
},
});
// 2) Validation boundary mapping
const parsed = await tryAsyncBoundary({
try: () => Promise.resolve(schema.parse(input)),
catch: (cause) => new ValidationError({ field: 'input', reason: String(cause) }),
});

Retry async operations without the full workflow engine:

import { tryAsyncRetry } from 'awaitly/result/retry';
const result = await tryAsyncRetry(
() => fetch('/api/data').then(r => r.json()),
(cause) => ({ type: 'FETCH_FAILED' as const, cause }),
{
retry: {
attempts: 4,
initialDelay: 100,
backoff: 'exponential',
shouldRetry: (e) => e.type === 'FETCH_FAILED',
},
}
);
FunctionPurpose
ok(value)Create success Result
err(error)Create failure Result
map(result, fn)Transform success value
mapError(result, fn)Transform error
andThen(result, fn)Chain operations
match(result, { ok, err })Pattern match
unwrap(result)Get value or throw
unwrapOr(result, default)Get value or default
from(fn)Wrap sync throwing code
fromPromise(promise)Wrap async throwing code
tryAsync(fn, mapError)Wrap with custom error
tryAsyncBoundary({ try, catch, retry? })Edge-first async wrapper with optional retry policy
tryAsyncRetry(fn, config)Wrap with retry support
all(results)Combine, fail on first error
partition(results)Separate successes/failures
flatten(nested)Unwrap nested Results
deserialize(value)Rehydrate from JSON/RPC

Learn about Workflows and Steps →