Skip to content

awaitly vs neverthrow

Both awaitly and neverthrow provide Result types for TypeScript. This guide compares their APIs and helps you choose the right tool.

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.

Featureawaitlyneverthrow
Result typeResult<T, E>Result<T, E>
Async resultAsyncResult<T, E>ResultAsync<T, E>
Method styleFunctionsMethods
Retry (standalone)tryAsyncRetry()Not included
Result serializationdeserialize() (typed errors)Not included
Flatten nested Resultsflatten()Not included
Workflow orchestrationBuilt-inNot included
Workflow error inference from depsYesNo
Step IDs + events / tracingYesNo
Concernneverthrowawaitly
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.

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 { Awaitly } from 'awaitly';
const result = divide(10, 2);
// Property-based
if (result.ok) {
console.log(result.value); // 5
}
// Function-based
if (Awaitly.isOk(result)) {
console.log(result.value); // 5
}
import { Awaitly } from 'awaitly';
const result = Awaitly.ok(5);
const doubled = Awaitly.map(result, n => n * 2);
/*
Output:
{ ok: true, value: 10 }
*/
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 } }
*/

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.

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 { Awaitly } from 'awaitly';
const result = Awaitly.ok(42);
const message = Awaitly.match(
result,
value => `Success: ${value}`,
error => `Error: ${error}`
);
/*
Output:
"Success: 42"
*/
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/await
const result = await fetchUser('123');
if (result.ok) {
console.log(result.value.name);
}
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 { Awaitly } from 'awaitly';
const result = Awaitly.ok(42);
// Throws if err
const value1 = Awaitly.unwrap(result); // 42
// Default value (does not throw)
const value2 = Awaitly.unwrapOr(result, 0); // 42
// Computed default
const value3 = Awaitly.unwrapOrElse(result, err => {
console.log('Failed:', err);
return 0;
}); // 42

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 naturally
const result = await Awaitly.andThen(parseId('42'), fetchUser);
/*
Output:
{ ok: true, value: { id: 42, name: 'Alice' } }
*/

Transform the success value with an async function.

import { Awaitly } from 'awaitly';
const result = Awaitly.ok(42);
// map works with async functions
const enriched = await Awaitly.map(result, async (n) => {
const data = await fetchMetadata(n);
return { value: n, metadata: data };
});
/*
Output:
{ ok: true, value: { value: 42, metadata: {...} } }
*/

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 error
const retyped = Awaitly.orElse(result, () => Awaitly.err('FALLBACK_FAILED'));
/*
Output:
{ ok: false, error: 'FALLBACK_FAILED' }
*/

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() instead
const failFast = Awaitly.all(results);
/*
Output:
{ ok: false, error: 'ERROR_A' }
*/

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');

neverthrow provides safeTry for generator-based composition. awaitly uses standard async/await instead.

import { run } from 'awaitly/run';
import { type ErrorsOf } from 'awaitly';
// awaitly uses familiar async/await — no generators needed
const deps = { parseId, fetchUser, sendEmail };
type Errors = ErrorsOf<typeof deps>;
const result = await run<User, Errors>(async ({ step }) => {
const id = await step('parseId', () => parseId('42'));
const user = await step('fetchUser', () => fetchUser(id));
await step('sendEmail', () => sendEmail(user.email));
return user;
});
// Errors short-circuit automatically, no special syntax required

With run(), group your deps and use ErrorsOf to derive the error union:

import { type AsyncResult, type ErrorsOf } from 'awaitly';
import { run } from 'awaitly/run';
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 deps = { fetchUser, sendEmail, chargeCard };
type Errors = ErrorsOf<typeof deps>;
const result = await run<User, Errors>(async ({ step }) => {
const user = await step('fetchUser', () => fetchUser('123'));
await step('chargeCard', () => chargeCard(99.99));
await step('sendEmail', () => sendEmail(user.email));
return user;
});
// result.error is: 'NOT_FOUND' | 'EMAIL_FAILED' | 'PAYMENT_DECLINED' | UnexpectedError

With createWorkflow, error inference is automatic — no ErrorsOf needed:

import { createWorkflow } from 'awaitly/workflow';
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 dependencies

Inside workflows, step is the workhorse. Chaining is just 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;
});
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 };
});
// 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/loadResumeState
import { 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);
// Restore
const loaded = await store.loadResumeState('wf-1');
if (loaded) {
await workflow.run(async ({ step, deps }) => { /* same fn */ }, { resumeState: loaded });
}
  • 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
  • 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.

// neverthrow: method chaining
import { 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.