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. The discriminant is type everywhere, string errors match themselves, tagged objects match on type, and TaggedError instances expose .type as the canonical field (._tag remains as a deprecated alias).
Two-arm catch-all:
import { match } from 'awaitly';
const result = divide(10, 2);const message = match(result, { ok: (value) => `Success: ${value}`, err: (error) => `Error: ${error}`,});// "Success: 5"Exhaustive per-type arms: dispatch across string literals, tagged objects, and TaggedError classes in one call:
import { match, ok, err, type Result } from 'awaitly';
type FetchUserError = 'USER_NOT_FOUND' | TimeoutError | UnexpectedError;
// `await` unwraps the AsyncResult, so the value here is a `Result`.const result: Result<User, FetchUserError> = await fetchUser('123');
const response = match(result, { ok: (user) => ({ status: 200, body: user }), USER_NOT_FOUND: () => ({ status: 404, body: { error: 'not_found' } }), TimeoutError: (e) => ({ status: 504, body: { error: 'timeout', ms: e.ms } }), UnexpectedError: (e) => ({ status: 500, body: { error: 'internal', cause: e.cause } }),});TypeScript errors if you omit a variant from the inferred union. See Tagged Errors for TaggedError-specific helpers.
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', 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' }Combining operations typed as Result<void, E> produces Result<void[], E>. The ts-eslint strict preset rejects void[] through @typescript-eslint/no-invalid-void-type. Use undefined for functions that return no value when you plan to combine their Results:
declare const revoke: (id: string) => Result<undefined, Forbidden>;
const revoked: Result<undefined[], Forbidden> = all(ids.map(revoke));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 at SDK Boundaries
Section titled “Edge Wrapping at SDK Boundaries”Wrap SDK/vendor edges with tryAsync so error classification lives at the boundary, then add the retry policy where the function is declared so classification and retry policy stay together:
import { tryAsync, retry } from 'awaitly';
const authorize = () => tryAsync( () => paymentProvider.authorize(card, total), (cause) => isTimeoutAfterCapture(cause) ? new PaymentLimbo({ attemptId, cause }) : new TransientVendorError({ vendor: 'stripe', cause }) );
const result = await retry(authorize, { attempts: 4, delay: 100, retryIf: (e) => e instanceof TransientVendorError,})();Boundary Cookbook (Copy/Paste)
Section titled “Boundary Cookbook (Copy/Paste)”// 1) Timeout-after-capture: classify as do-not-retryconst authorizePayment = () => tryAsync( () => gateway.authorize(card, total), (cause) => isTimeoutAfterCapture(cause) ? new PaymentLimbo({ attemptId, cause }) : new TransientVendorError({ vendor: 'stripe', cause }) );
const payment = await retry(authorizePayment, { attempts: 3, delay: 100, backoff: 'fixed', retryIf: (e) => e instanceof TransientVendorError,})();
// 2) Validation boundary mappingconst parsed = await tryAsync( () => Promise.resolve(schema.parse(input)), (cause) => new ValidationError({ field: 'input', reason: String(cause) }));Retry with the retry Policy
Section titled “Retry with the retry Policy”Retry async operations without the full workflow engine. retry wraps any Result-returning (or plain) function and keeps its error union unchanged, if all attempts fail, the last failure propagates:
import { tryAsync, retry } from 'awaitly';
const fetchData = () => tryAsync( () => fetch('/api/data').then(r => r.json()), (cause) => ({ type: 'FETCH_FAILED', cause }) );
const result = await retry(fetchData, { attempts: 4, delay: 100, backoff: 'exponential',})();By default, retry skips UnexpectedError and untagged throws. Typed errors still retry. Pass retryIf: () => true to opt back into retrying throws. In tests, pass createTestClock() as clock so delays do not wait on real time. Policy wrappers do not pick up run({ clock }).
The same wrapped function drops straight into a run() deps object, so the policy is visible where the dependency is declared.
Timeout with the timeout Policy
Section titled “Timeout with the timeout Policy”Bound a dependency’s execution time. On timeout, resolves to err(TimeoutError), adding TimeoutError to the error union:
import { tryAsync, timeout } from 'awaitly';
const fetchData = () => tryAsync( () => fetch('/api/data').then(r => r.json()), (cause) => ({ type: 'FETCH_FAILED', cause }) );
const result = await timeout(fetchData, 5000)();// result.error may be 'FETCH_FAILED' | TimeoutErrorAccepts milliseconds or duration strings ('5s', '500ms'). Optional third argument { clock } uses that clock for the deadline instead of wall time.
Fallback with the fallback Policy
Section titled “Fallback with the fallback Policy”Recover from a dependency’s failure. The handler receives the failure plus original arguments; base errors are consumed and only handler errors remain in the union:
import { ok, fallback, tryAsync } from 'awaitly';
const fetchPrimary = () => tryAsync( () => primaryCache.get(key), () => ({ type: 'CACHE_MISS' }) );
const result = await fallback(fetchPrimary, () => ok({ value: 'default' }))();// On CACHE_MISS, returns ok({ value: 'default' }) — no typed errors leftComposing policies
Section titled “Composing policies”Policies nest inside-out. Timeout applies per attempt when wrapped by retry:
import { retry, timeout } from 'awaitly';
const deps = { charge: retry(timeout(charge, 5000), { attempts: 3, backoff: 'exponential', delay: 100, }),};awaitly-analyze reads policy chains from the deps literal and records them on DependencyInfo.policies for diagrams and diagnostics.
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 }) |
Two-arm pattern match |
match(result, { ok, Variant, ... }) |
Exhaustive per-type 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 |
retry(fn, options) |
Per-dep retry policy (also works standalone) |
timeout(fn, ms) |
Per-dep timeout policy; adds TimeoutError |
fallback(fn, handler) |
Per-dep fallback; consumes base errors |
all(results) |
Combine, fail on first error |
partition(results) |
Separate successes/failures |
flatten(nested) |
Unwrap nested Results |
deserialize(value) |
Rehydrate from JSON/RPC |