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 is a data type: Result<T, E>, combinators, and explicit error values. It does not 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)
In other words: neverthrow wraps results; awaitly models effects. That’s the structural difference. neverthrow is ideal for local operations and clear error returns; awaitly addresses application-level composition and environment so that as your app grows, you have one coherent abstraction for async + env + errors.
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 (standalone) | tryAsyncRetry() | 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 | ❌ | ❌ |
So: neverthrow is the smallest step from plain async/await if you only want Result types. awaitly is a bigger structural step when you want one model for composition, environment, and errors — a minimal effect-style approach for TypeScript apps.
Creating Results
Section titled “Creating Results”import { Awaitly } from 'awaitly';
const divide = (a: number, b: number) => b === 0 ? Awaitly.err('DIVIDE_BY_ZERO') : Awaitly.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 { Awaitly } from 'awaitly';
const result = divide(10, 2);
// Property-basedif (result.ok) { console.log(result.value); // 5}
// Function-basedif (Awaitly.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 { Awaitly } from 'awaitly';
const result = Awaitly.ok(5);const doubled = Awaitly.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 { Awaitly } from 'awaitly';
const result = Awaitly.err('NOT_FOUND');const mapped = Awaitly.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 } }*/Pipeline style (awaitly)
Section titled “Pipeline style (awaitly)”awaitly provides Awaitly.pipe and Awaitly.R for left-to-right composition. Awaitly.R exposes curried combinators that work inside pipe:
import { Awaitly } from 'awaitly';
const result = Awaitly.pipe( Awaitly.ok(5), Awaitly.R.map((n) => n * 2), Awaitly.R.mapError((e) => e.toUpperCase()));// { ok: true, value: 10 }See Functional Utilities for the full set of combinators and async helpers.
Chaining Operations
Section titled “Chaining Operations”andThen / flatMap
Section titled “andThen / flatMap”import { Awaitly } from 'awaitly';
const parseNumber = (s: string) => { const n = parseInt(s, 10); return isNaN(n) ? Awaitly.err('PARSE_ERROR') : Awaitly.ok(n);};
const result = Awaitly.ok('42');const parsed = Awaitly.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 { Awaitly } from 'awaitly';
const result = Awaitly.ok(42);const message = Awaitly.match( result, value => `Success: ${value}`, 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 { Awaitly, type AsyncResult } from 'awaitly';
const fetchUser = async (id: string): AsyncResult<User, 'NOT_FOUND'> => { const user = await db.find(id); return user ? Awaitly.ok(user) : Awaitly.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 { Awaitly } from 'awaitly';
const results = [Awaitly.ok(1), Awaitly.ok(2), Awaitly.ok(3)];const combined = Awaitly.all(results);/*Output:{ ok: true, value: [1, 2, 3] }*/
const withError = [Awaitly.ok(1), Awaitly.err('FAILED'), Awaitly.ok(3)];const failed = Awaitly.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 { Awaitly } from 'awaitly';
const result = Awaitly.ok(42);
// Throws if errconst value1 = Awaitly.unwrap(result); // 42
// Default value (does not throw)const value2 = Awaitly.unwrapOr(result, 0); // 42
// Computed defaultconst value3 = Awaitly.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 { Awaitly, type AsyncResult } from 'awaitly';
const parseId = (s: string) => { const n = parseInt(s, 10); return isNaN(n) ? Awaitly.err('PARSE_ERROR') : Awaitly.ok(n);};
const fetchUser = async (id: number): AsyncResult<User, 'NOT_FOUND'> => { const user = await db.find(id); return user ? Awaitly.ok(user) : Awaitly.err('NOT_FOUND');};
// Just use async function - awaitly handles sync→async naturallyconst result = await Awaitly.andThen(parseId('42'), fetchUser);/*Output:{ ok: true, value: { id: 42, name: 'Alice' } }*/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 { Awaitly } from 'awaitly';
const result = Awaitly.ok(42);
// map works with async functionsconst enriched = await Awaitly.map(result, async (n) => { const data = await fetchMetadata(n); return { value: n, metadata: data };});/*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 { Awaitly } from 'awaitly';
const result = Awaitly.err('NOT_FOUND');const recovered = Awaitly.orElse(result, (e) => Awaitly.ok({ fallback: true, reason: e }));/*Output:{ ok: true, value: { fallback: true, reason: 'NOT_FOUND' } }*/
// Can also return a different errorconst retyped = Awaitly.orElse(result, () => Awaitly.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 { Awaitly } from 'awaitly';
const results = [Awaitly.ok(1), Awaitly.err('ERROR_A'), Awaitly.ok(3), Awaitly.err('ERROR_B')];const settled = Awaitly.allSettled(results);// Returns a Result with collected errors (and, depending on API, may include successes/failures).// See [Awaitly.allSettled](/reference/api/) for the exact return shape.
// For fail-fast behavior, use all() insteadconst failFast = Awaitly.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 { Awaitly } from 'awaitly';
// Sync: Awaitly.from(fn, onError)const parseJson = (s: string) => Awaitly.from( () => JSON.parse(s), (e) => ({ type: 'PARSE_ERROR' as const, message: String(e) }) );
// Async (outside workflows): Awaitly.fromPromise(promise, onError)const fetchSafe = (url: string) => Awaitly.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 { createWorkflow } from 'awaitly/workflow';
// awaitly uses familiar async/await - no generators 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;});
// Errors short-circuit automatically, no special syntax requiredimport { 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”import { Awaitly, type AsyncResult } from 'awaitly';import { createWorkflow } from 'awaitly/workflow';
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'> => { /* ... */ };
// Error types automatically inferred from all dependenciesconst 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;});
// result.error is: 'NOT_FOUND' | 'EMAIL_FAILED' | 'PAYMENT_DECLINED' | UnexpectedError// ^^^ Automatically inferred from dependencies!Step helpers (run, andThen, match)
Section titled “Step helpers (run, andThen, match)”Inside workflows you can use a neverthrow-style API: step.run (unwrap), step.andThen (chain), step.match (pattern match). They run through the full step engine (events, retry, cache).
const result = await workflow.run(async ({ step, deps }) => { const user = await step.run('fetchUser', () => deps.fetchUser('123')); const enriched = await step.andThen('enrich', user, (u) => deps.enrichUser(u)); return step.match('format', enriched, { ok: (e) => e.displayName, err: () => 'Unknown', });});See Steps — Effect-style ergonomics.
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', delayMs: 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
Bottom line: neverthrow helps you return errors; awaitly helps you structure applications.
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 { Awaitly } from 'awaitly';
const base = Awaitly.ok(5);const doubled = Awaitly.map(base, (n) => n * 2);const result = Awaitly.mapError(doubled, (e) => e.toUpperCase());For pipeline style, use Awaitly.pipe and Awaitly.R as shown in Pipeline style (awaitly) above, or a small pipe helper of your own to compose Awaitly.map / Awaitly.mapError.