awaitly vs neverthrow
Both awaitly and neverthrow provide Result types for TypeScript. This guide compares their APIs and helps you choose the right tool.
The Conceptual Difference
Section titled “The Conceptual Difference”neverthrow gives you a way to represent success or failure. It’s a data type: Result<T, E>, combinators, and explicit error values. It doesn’t structure your app, provide dependency injection, or model effectful computation. It’s a tool for wrapping results.
awaitly gives you a way to model computations that:
- run asynchronously
- depend on an explicit environment (deps)
- may fail with a typed error
- compose predictably (workflows, steps, retries, persistence)
neverthrow wraps results; awaitly models effects. neverthrow is a good fit for local operations and clear error returns. awaitly takes on application-level composition and environment, so as your app grows you keep one consistent way to handle async, dependencies, and errors together.
Quick Comparison
Section titled “Quick Comparison”| Feature | awaitly | neverthrow |
|---|---|---|
| Result type | Result<T, E> |
Result<T, E> |
| Async result | AsyncResult<T, E> |
ResultAsync<T, E> |
| Method style | Functions | Methods |
| Retry | per-dep retry() policy |
Not included |
| Result serialization | deserialize() (typed errors) |
Not included |
| Flatten nested Results | flatten() |
Not included |
| Workflow orchestration | Built-in | Not included |
| Workflow error inference from deps | Yes | No |
| Step IDs + events / tracing | Yes | No |
When to choose which
Section titled “When to choose which”| Concern | neverthrow | awaitly |
|---|---|---|
| Typed errors | ✅ | ✅ |
| Async composition | ⚠️ manual (ResultAsync, safeTry, etc.) | ✅ built-in (workflows, steps) |
| Dependency injection | ❌ | ✅ (deps at creation or per run) |
| Unified abstraction (async + env + errors) | ❌ | ✅ |
| Runtime / framework | ❌ | ❌ |
neverthrow is the smallest step from plain async/await if you only want Result types. awaitly is a bigger step: one model for composition, environment, and errors, in a minimal effect style for TypeScript apps.
Creating Results
Section titled “Creating Results”import { ok, err } from 'awaitly';
const divide = (a: number, b: number) => b === 0 ? err('DIVIDE_BY_ZERO') : ok(a / b);
const result = divide(10, 2);/*Output:{ ok: true, value: 5 }*/import { ok, err } from 'neverthrow';
const divide = (a: number, b: number) => b === 0 ? err('DIVIDE_BY_ZERO') : ok(a / b);
const result = divide(10, 2);/*Output:Result { _value: 5 }*/Checking Results
Section titled “Checking Results”import { isOk } from 'awaitly';
const result = divide(10, 2);
// Property-basedif (result.ok) { console.log(result.value); // 5}
// Function-basedif (isOk(result)) { console.log(result.value); // 5}const result = divide(10, 2);
// Method-basedif (result.isOk()) { console.log(result.value); // 5}
if (result.isErr()) { console.log(result.error);}Transforming Values
Section titled “Transforming Values”import { ok, map } from 'awaitly';
const result = ok(5);const doubled = map(result, n => n * 2);/*Output:{ ok: true, value: 10 }*/import { ok } from 'neverthrow';
const result = ok(5);const doubled = result.map(n => n * 2);/*Output:Result { _value: 10 }*/mapError
Section titled “mapError”import { err, mapError } from 'awaitly';
const result = err('NOT_FOUND');const mapped = mapError(result, e => ({ code: e, status: 404 }));/*Output:{ ok: false, error: { code: 'NOT_FOUND', status: 404 } }*/import { err } from 'neverthrow';
const result = err('NOT_FOUND');const mapped = result.mapErr(e => ({ code: e, status: 404 }));/*Output:Result { _error: { code: 'NOT_FOUND', status: 404 } }*/No pipeline operator (awaitly)
Section titled “No pipeline operator (awaitly)”awaitly deliberately ships no pipe. Short chains are plain function calls with the named combinators:
import { ok, map, mapError } from 'awaitly';
const doubled = map(ok(5), (n) => n * 2);const result = mapError(doubled, (e) => e.toUpperCase());// { ok: true, value: 10 }For anything longer, especially async, the answer is not a pipeline at all. Plain async/await with run() gives sequencing, short-circuiting, and inferred error types without a second dialect (see Generator-Based Composition below).
Chaining Operations
Section titled “Chaining Operations”andThen / flatMap
Section titled “andThen / flatMap”import { ok, err, andThen } from 'awaitly';
const parseNumber = (s: string) => { const n = parseInt(s, 10); return isNaN(n) ? err('PARSE_ERROR') : ok(n);};
const result = ok('42');const parsed = andThen(result, parseNumber);/*Output:{ ok: true, value: 42 }*/import { ok, err } from 'neverthrow';
const parseNumber = (s: string) => { const n = parseInt(s, 10); return isNaN(n) ? err('PARSE_ERROR') : ok(n);};
const result = ok('42');const parsed = result.andThen(parseNumber);/*Output:Result { _value: 42 }*/Pattern Matching
Section titled “Pattern Matching”import { ok, match } from 'awaitly';
const result = ok(42);const message = match(result, { ok: value => `Success: ${value}`, err: error => `Error: ${error}`,});/*Output:"Success: 42"*/import { ok } from 'neverthrow';
const result = ok(42);const message = result.match( value => `Success: ${value}`, error => `Error: ${error}`);/*Output:"Success: 42"*/Async Operations
Section titled “Async Operations”import { ok, err, type AsyncResult } from 'awaitly';
const fetchUser = async (id: string): AsyncResult<User, 'NOT_FOUND'> => { const user = await db.find(id); return user ? ok(user) : err('NOT_FOUND');};
// Use with regular async/awaitconst result = await fetchUser('123');if (result.ok) { console.log(result.value.name);}import { ok, err, ResultAsync } from 'neverthrow';
const fetchUser = (id: string): ResultAsync<User, 'NOT_FOUND' | 'DB_ERROR'> => ResultAsync.fromPromise(db.find(id), () => 'DB_ERROR' as const) .andThen((user) => (user ? ok(user) : err('NOT_FOUND' as const)));
const result = await fetchUser('123');if (result.isOk()) { console.log(result.value.name);}Batch Operations
Section titled “Batch Operations”Combining Multiple Results
Section titled “Combining Multiple Results”import { ok, err, all } from 'awaitly';
const results = [ok(1), ok(2), ok(3)];const combined = all(results);/*Output:{ ok: true, value: [1, 2, 3] }*/
const withError = [ok(1), err('FAILED'), ok(3)];const failed = all(withError);/*Output:{ ok: false, error: 'FAILED' }*/import { ok, err, Result } from 'neverthrow';
const results = [ok(1), ok(2), ok(3)];const combined = Result.combine(results);/*Output:Result { _value: [1, 2, 3] }*/
const withError = [ok(1), err('FAILED'), ok(3)];const failed = Result.combine(withError);/*Output:Result { _error: 'FAILED' }*/Unwrapping
Section titled “Unwrapping”import { ok, unwrap, unwrapOr, unwrapOrElse } from 'awaitly';
const result = ok(42);
// Throws if errconst value1 = unwrap(result); // 42
// Default value (does not throw)const value2 = unwrapOr(result, 0); // 42
// Computed defaultconst value3 = unwrapOrElse(result, err => { console.log('Failed:', err); return 0;}); // 42import { ok } from 'neverthrow';
const result = ok(42);
// Default value (does not throw)const value1 = result.unwrapOr(0); // 42
// Pattern matchconst value2 = result.match( (v) => v, (_e) => 0); // 42
// In tests only (throws on Err)const value3 = result._unsafeUnwrap(); // 42awaitly: unwrap() throws on Err; unwrapOr/unwrapOrElse never throw. neverthrow’s throwing equivalent is _unsafeUnwrap() (intended for tests); for computed defaults use match().
Async Chaining
Section titled “Async Chaining”asyncAndThen
Section titled “asyncAndThen”Chain a sync Result into an async operation.
import { ok, err, run, type AsyncResult } from 'awaitly';
const parseId = (s: string) => { const n = parseInt(s, 10); return isNaN(n) ? err('PARSE_ERROR') : ok(n);};
const fetchUser = async (id: number): AsyncResult<User, 'NOT_FOUND'> => { const user = await db.find(id); return user ? ok(user) : err('NOT_FOUND');};
// Sync and async deps mix freely — await handles bothconst result = await run({ parseId, fetchUser }, async (s) => { const id = await s.parseId('42'); return s.fetchUser(id);});/*Output:{ ok: true, value: { id: 42, name: 'Alice' } }result.error: 'PARSE_ERROR' | 'NOT_FOUND' | UnexpectedError — inferred*/import { ok, err, ResultAsync } from 'neverthrow';
const parseId = (s: string) => { const n = parseInt(s, 10); return isNaN(n) ? err('PARSE_ERROR' as const) : ok(n);};
const fetchUser = (id: number) => ResultAsync.fromPromise(db.find(id), () => 'NOT_FOUND' as const);
// Use asyncAndThen to chain sync Result → ResultAsyncconst result = await parseId('42').asyncAndThen(fetchUser);/*Output:Result { _value: { id: 42, name: 'Alice' } }*/asyncMap
Section titled “asyncMap”Transform the success value with an async function.
import { ok } from 'awaitly';
const result = ok(42);
// Plain async/await — no dedicated asyncMap combinator neededconst enriched = result.ok ? ok({ value: result.value, metadata: await fetchMetadata(result.value) }) : result;/*Output:{ ok: true, value: { value: 42, metadata: {...} } }*/import { ok } from 'neverthrow';
const result = ok(42);
// Use asyncMap for async transformationsconst enriched = await result.asyncMap(async (n) => { const data = await fetchMetadata(n); return { value: n, metadata: data };});/*Output:Result { _value: { value: 42, metadata: {...} } }*/Error Recovery
Section titled “Error Recovery”orElse
Section titled “orElse”Provide a fallback Result on failure.
import { ok, err, orElse } from 'awaitly';
const result = err('NOT_FOUND');const recovered = orElse(result, (e) => ok({ fallback: true, reason: e }));/*Output:{ ok: true, value: { fallback: true, reason: 'NOT_FOUND' } }*/
// Can also return a different errorconst retyped = orElse(result, () => err('FALLBACK_FAILED'));/*Output:{ ok: false, error: 'FALLBACK_FAILED' }*/import { err, ok } from 'neverthrow';
const result = err('NOT_FOUND');const recovered = result.orElse((e) => ok({ fallback: true, reason: e }));/*Output:Result { _value: { fallback: true, reason: 'NOT_FOUND' } }*/
// Can also return a different errorconst retyped = result.orElse(() => err('FALLBACK_FAILED'));/*Output:Result { _error: 'FALLBACK_FAILED' }*/Collecting All Errors
Section titled “Collecting All Errors”combineWithAllErrors
Section titled “combineWithAllErrors”Collect ALL errors instead of failing on the first one.
import { ok, err, all, allSettled } from 'awaitly';
const results = [ok(1), err('ERROR_A'), ok(3), err('ERROR_B')];const settled = allSettled(results);/*Output:{ ok: false, error: [{ error: 'ERROR_A' }, { error: 'ERROR_B' }] }*/
// For fail-fast behavior, use all() insteadconst failFast = all(results);/*Output:{ ok: false, error: 'ERROR_A' }*/import { ok, err, Result } from 'neverthrow';
const results = [ok(1), err('ERROR_A'), ok(3), err('ERROR_B')];const combined = Result.combineWithAllErrors(results);/*Output:Result { _error: ['ERROR_A', 'ERROR_B'] }*/
// For fail-fast behavior, use combine() insteadconst failFast = Result.combine(results);/*Output:Result { _error: 'ERROR_A' }*/Wrapping Throwing Functions
Section titled “Wrapping Throwing Functions”fromThrowable / from / fromPromise
Section titled “fromThrowable / from / fromPromise”Safely wrap functions that might throw exceptions.
import { from, fromPromise } from 'awaitly';
// Sync: from(fn, onError)const parseJson = (s: string) => from( () => JSON.parse(s), (e) => ({ type: 'PARSE_ERROR' as const, message: String(e) }) );
// Async (outside workflows): fromPromise(promise, onError)const fetchSafe = (url: string) => fromPromise(fetch(url).then(r => r.json()), () => 'FETCH_ERROR' as const);
// Inside workflows: step.try('id', () => …, { error: 'MY_ERROR' })
const valid = parseJson('{"name": "Alice"}');const invalid = parseJson('not json');import { Result, ResultAsync } from 'neverthrow';
// Sync: Result.fromThrowable(fn, onError)const safeJsonParse = Result.fromThrowable( JSON.parse, (e) => ({ type: 'PARSE_ERROR' as const, message: String(e) }));
// Async: ResultAsync.fromPromise(promise, onError)const fetchSafe = (url: string) => ResultAsync.fromPromise(fetch(url).then(r => r.json()), () => 'FETCH_ERROR' as const);
const valid = safeJsonParse('{"name": "Alice"}');const invalid = safeJsonParse('not json');Generator-Based Composition
Section titled “Generator-Based Composition”safeTry / Generators
Section titled “safeTry / Generators”neverthrow provides safeTry for generator-based composition. awaitly uses standard async/await instead.
import { run } from 'awaitly';
// awaitly uses familiar async/await — no generators neededconst result = await run({ parseId, fetchUser, sendEmail }, async (s) => { const id = await s.parseId('42'); const user = await s.fetchUser(id); await s.sendEmail(user.email); return user;});
// Errors short-circuit automatically, no special syntax required,// and result.error is inferred from the depsimport { createWorkflow } from 'awaitly';
// createWorkflow infers errors automatically — no ErrorsOf neededconst workflow = createWorkflow('workflow', { parseId, fetchUser, sendEmail });
const result = await workflow.run(async ({ step, deps }) => { const id = await step('parseId', () => deps.parseId('42')); const user = await step('fetchUser', () => deps.fetchUser(id)); await step('sendEmail', () => deps.sendEmail(user.email)); return user;});import { safeTry, ok } from 'neverthrow';
// parseId, fetchUser, sendEmail must return Result or ResultAsync for safeTryconst result = safeTry(function* () { const id = yield* parseId('42'); // yield* the Result directly const user = yield* fetchUser(id); // Result or ResultAsync supported yield* sendEmail(user.email); return ok(user);});What awaitly Adds: Workflow Orchestration
Section titled “What awaitly Adds: Workflow Orchestration”Automatic Error Type Inference
Section titled “Automatic Error Type Inference”With run(), pass your deps as the first argument. The error union is inferred, with no type parameters needed:
import { run, type AsyncResult } from 'awaitly';
const fetchUser = async (id: string): AsyncResult<User, 'NOT_FOUND'> => { /* ... */ };const sendEmail = async (to: string): AsyncResult<void, 'EMAIL_FAILED'> => { /* ... */ };const chargeCard = async (amount: number): AsyncResult<Receipt, 'PAYMENT_DECLINED'> => { /* ... */ };
const result = await run({ fetchUser, sendEmail, chargeCard }, async (s) => { const user = await s.fetchUser('123'); await s.chargeCard(99.99); await s.sendEmail(user.email); return user;});
// result.error is: 'NOT_FOUND' | 'EMAIL_FAILED' | 'PAYMENT_DECLINED' | UnexpectedErrorWith createWorkflow, error inference is automatic, no ErrorsOf needed:
import { createWorkflow } from 'awaitly';
const workflow = createWorkflow('workflow', { fetchUser, sendEmail, chargeCard });
const result = await workflow.run(async ({ step, deps }) => { const user = await step('fetchUser', () => deps.fetchUser('123')); await step('chargeCard', () => deps.chargeCard(99.99)); await step('sendEmail', () => deps.sendEmail(user.email)); return user;});
// Same error type, automatically inferred from dependenciesStep helpers
Section titled “Step helpers”Inside workflows, step is the workhorse. Chaining is calling step again with the success value. Pattern matching is plain JS branching after the step returns.
const result = await workflow.run(async ({ step, deps }) => { const user = await step('fetchUser', () => deps.fetchUser('123')); const enriched = await step('enrich', () => deps.enrichUser(user)); return enriched.displayName;});Built-in Retry and Timeout
Section titled “Built-in Retry and Timeout”const result = await workflow.run(async ({ step, deps }) => { // Retry with exponential backoff const user = await step.retry( 'fetchUser', () => deps.fetchUser('123'), { attempts: 3, backoff: 'exponential', initialDelay: 100 } );
// Timeout protection const data = await step.withTimeout( 'slowOp', () => deps.slowOperation(), { ms: 5000 } );
return { user, data };});Step Caching and Resume
Section titled “Step Caching and Resume”// Option 1: In-memory (simple)const workflow = createWorkflow('workflow', deps, { cache: new Map(), resumeState: savedState, // Resume from previous run});
// Option 2: Store (awaitly-mongo or awaitly-postgres) — runWithState + save/loadResumeStateimport { mongo } from 'awaitly-mongo';// or: import { postgres } from 'awaitly-postgres';
const store = mongo(process.env.MONGODB_URI!);
const { result, resumeState } = await workflow.runWithState(async ({ step, deps }) => { const user = await step('fetchUser', () => deps.fetchUser('1'), { key: 'user:1' }); return user;});await store.save('wf-1', resumeState);
// Restoreconst loaded = await store.loadResumeState('wf-1');if (loaded) { await workflow.run(async ({ step, deps }) => { /* same fn */ }, { resumeState: loaded });}When to Choose Each
Section titled “When to Choose Each”Choose awaitly when:
Section titled “Choose awaitly when:”- You want Result types with familiar async/await syntax
- You need workflow orchestration (retries, timeouts, caching)
- You want automatic error type inference
- You’re building multi-step async operations
- You need step-level resilience patterns
- You want to keep async/await ergonomics for multi-step flows
Choose neverthrow when:
Section titled “Choose neverthrow when:”- You only need Result types without workflow features
- You prefer method chaining over function calls
- You want the smallest possible bundle size
- Your project already uses neverthrow
neverthrow helps you return errors. awaitly helps you structure whole applications around them.
Method Chaining vs Functions
Section titled “Method Chaining vs Functions”// neverthrow: method chainingimport { ok } from 'neverthrow';const result = ok(5).map(n => n * 2).mapErr(e => e.toUpperCase());
// awaitly: standalone functions (no special helpers required)import { ok, map, mapError } from 'awaitly';
const base = ok(5);const doubled = map(base, (n) => n * 2);const result = mapError(doubled, (e) => e.toUpperCase());There is no pipeline operator to learn: intermediate values are ordinary const bindings, and multi-step async flows use run() (see No pipeline operator (awaitly) above).