Result Types
Every function that can fail has to answer one question: how does it tell you what went wrong? try/catch hides the answer: the function throws, and you find out at runtime. A Result puts the answer in the return value instead.
A Result is a plain object with one of two shapes:
// success{ ok: true, value: 5 }
// failure{ ok: false, error: 'DIVIDE_BY_ZERO' }You check result.ok to see which one you got. There’s nothing magic here. You could write these objects by hand. awaitly gives you ok() and err() to build them, plus helpers to work with them safely.
New to the type syntax?
Section titled “New to the type syntax?”You’ll see types like Result<number, 'DIVIDE_BY_ZERO'> throughout this page. If TypeScript’s notation is new to you, three pieces are worth knowing first:
- A specific string can be a type. A value typed as
'DIVIDE_BY_ZERO'can only ever be that exact string. The string is both a value and its own type. |means “one of”.'A' | 'B'describes a value that is either'A'or'B', and nothing else. TypeScript calls this a union.- The angle brackets fill in blanks.
Resultneeds two types to be complete: the success value, then the error. You pass them in that order, soResult<number, 'DIVIDE_BY_ZERO'>reads as “a number if it worked, or'DIVIDE_BY_ZERO'if it didn’t.”
Take your time with these. The rest of the page builds on them.
Why Results?
Section titled “Why Results?”// 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}`);}try { const user = await fetchUser('123'); console.log(user.name);} catch (error) { // What type is error? TypeScript doesn't know console.log(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
Type Structure
Section titled “Type Structure” ┌─── The success value type │ ┌─── The error type ▼ ▼Result<Value, Error>
Examples:Result<number, 'DIVIDE_BY_ZERO'> → number on success, 'DIVIDE_BY_ZERO' on failureResult<User, 'NOT_FOUND'> → User on success, 'NOT_FOUND' on failureResult<Order, 'INVALID' | 'EXPIRED'> → Order on success, one of two errors on failureCreating Results
Section titled “Creating Results”import { ok, err, type AsyncResult } from 'awaitly';
// Synchronousconst divide = (a: number, b: number): Result<number, 'DIVIDE_BY_ZERO'> => b === 0 ? err('DIVIDE_BY_ZERO') : ok(a / b);
// Asynchronousconst fetchUser = async (id: string): AsyncResult<User, 'NOT_FOUND'> => { const user = await db.users.find(id); return user ? ok(user) : err('NOT_FOUND');};Checking Results
Section titled “Checking Results”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.okistrue, you can accessresult.value - If
result.okisfalse, you can accessresult.error
Type Guards
Section titled “Type Guards”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'}Transforming Results
Section titled “Transforming Results”map - Transform the Value
Section titled “map - Transform the Value”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 }mapError - Transform the Error
Section titled “mapError - Transform the Error”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 } }andThen - Chain Operations
Section titled “andThen - Chain Operations”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' }match - Pattern Match
Section titled “match - Pattern Match”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"Unwrapping
Section titled “Unwrapping”unwrap - Get Value or Throw
Section titled “unwrap - Get Value or Throw”import { unwrap } from 'awaitly';
const result = divide(10, 2);const value = unwrap(result); // 5
const badResult = divide(10, 0);const boom = unwrap(badResult); // Throws!unwrapOr - Get Value or Default
Section titled “unwrapOr - Get Value or Default”Return a default value on error:
import { unwrapOr } from 'awaitly';
const result = divide(10, 0);const value = unwrapOr(result, 0); // 0unwrapOrElse - 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: 0Wrapping Throwing Code
Section titled “Wrapping Throwing Code”from - Wrap Sync Functions
Section titled “from - Wrap Sync Functions”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 }fromPromise - Wrap Async Functions
Section titled “fromPromise - Wrap Async Functions”import { fromPromise } from 'awaitly';
const result = await fromPromise(fetch('/api/data'));// Success: { ok: true, value: Response }// Network error: { ok: false, error: TypeError }tryAsync - Wrap with Custom Error
Section titled “tryAsync - Wrap with Custom Error”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 } }Batch Operations
Section titled “Batch Operations”all - Fail Fast
Section titled “all - Fail Fast”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' }allSettled - Collect All Outcomes
Section titled “allSettled - Collect All Outcomes”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']Async Versions
Section titled “Async Versions”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' }Flatten
Section titled “Flatten”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' }Deserialization
Section titled “Deserialization”Rehydrate Results from JSON, RPC, or server actions with type-safe error handling:
import { deserialize, DESERIALIZATION_ERROR } from 'awaitly/result';
// Valid serialized Resultconst result = deserialize<User, AppError>(rpcResponse);if (result.ok) { console.log(result.value); // User}
// Invalid input returns a typed DeserializationErrorconst invalid = deserialize({ foo: 'bar' });if (!invalid.ok && invalid.error.type === DESERIALIZATION_ERROR) { console.log('Bad input:', invalid.error.value);}Edge Wrapping with tryAsyncBoundary
Section titled “Edge Wrapping with tryAsyncBoundary”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, },});Boundary Cookbook (Copy/Paste)
Section titled “Boundary Cookbook (Copy/Paste)”// 1) Timeout-after-capture: classify as do-not-retryconst 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 mappingconst parsed = await tryAsyncBoundary({ try: () => Promise.resolve(schema.parse(input)), catch: (cause) => new ValidationError({ field: 'input', reason: String(cause) }),});Retry with tryAsyncRetry
Section titled “Retry with tryAsyncRetry”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', }, });Summary
Section titled “Summary”| Function | Purpose |
|---|---|
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 |