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.

// 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,
(value) => `Success: ${value}`,
(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' }

Always succeeds, returning every outcome:

import { allSettled } from 'awaitly';
const results = [ok(1), err('A'), ok(3), err('B')];
const settled = allSettled(results);
// {
// ok: true,
// value: [
// { status: 'ok', value: 1 },
// { status: 'err', error: 'A' },
// { status: 'ok', value: 3 },
// { status: 'err', error: 'B' },
// ]
// }

partition - Separate Successes and Failures

Section titled “partition - Separate Successes and Failures”
import { partition } from 'awaitly';
const results = [ok(1), err('A'), ok(3)];
const [successes, failures] = partition(results);
// successes: [1, 3]
// failures: ['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);
}

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: {
times: 3,
delayMs: 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, onOk, onErr)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
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 →