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 { createWorkflow } from 'awaitly/workflow';
// awaitly uses familiar async/await - no generators needed
const 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 required
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 dependencies
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;
});
// result.error is: 'NOT_FOUND' | 'EMAIL_FAILED' | 'PAYMENT_DECLINED' | UnexpectedError
// ^^^ Automatically inferred from dependencies!

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.

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 };
});
// 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.