Skip to content

awaitly vs Promises

Promises are JavaScript’s native way to handle async operations — they’re excellent at representing async completion and composing via .then / await. awaitly’s AsyncResult builds on Promises to add typed errors and an explicit environment. This guide compares both approaches.

A Promise models time. It says “this async computation will eventually produce a value, or reject.” That’s the whole contract.

awaitly models more than time. Its type says what the computation needs, what it can fail with, and what it produces. Promises give you eventual values; awaitly adds typed failure and an explicit environment on top.

Where Promises stop short (they aren’t “wrong”, just incomplete):

  1. Rejection is unstructured: any value can be thrown. Errors aren’t declared in the type, so Promise<T> tells you nothing about failure modes.
  2. Composition loses failure information: when you chain or combine promises, failure types widen to unknown, and you inspect errors at runtime.
  3. Dependencies are invisible: a function returning Promise<T> doesn’t tell you what services or context it depends on. That becomes a scaling problem in larger apps.

awaitly doesn’t replace Promises. It builds on them: Promises stay the foundation, and awaitly adds a second typed channel for failure and an explicit environment.

Promise + try/catch AsyncResult
┌─────────────────────────┐ ┌──────────────────────────┐
│ Exceptions are hidden │ │ Errors are visible │
│ ───────────────────── │ │ ────────────────────── │
│ • Happy path focus │ │ • Errors in return type │
│ • Catch blocks handle │ │ • TypeScript tracks │
│ • Runtime discovery │ │ • Compile-time safety │
│ • Implicit failure │ │ • Explicit failure │
└─────────────────────────┘ └──────────────────────────┘
Feature Promise + try/catch awaitly AsyncResult
Error visibility Hidden in runtime In return type
Type safety Promise<T> AsyncResult<T, E>
Error type unknown in catch Typed E
Composition try/catch nesting all, andThen, pipe
Learning curve Familiar Low (uses async/await)
import { ok, err, type AsyncResult } from 'awaitly';
const fetchUser = async (id: string): AsyncResult<User, 'NOT_FOUND' | 'DB_ERROR'> => {
try {
const user = await db.find(id);
return user ? ok(user) : err('NOT_FOUND');
} catch {
return err('DB_ERROR');
}
};
// Caller sees all possible errors in the type
const result = await fetchUser('123');
// ^? AsyncResult<User, 'NOT_FOUND' | 'DB_ERROR'>
if (result.ok) {
console.log(result.value.name);
} else {
// TypeScript knows: result.error is 'NOT_FOUND' | 'DB_ERROR'
console.log(result.error);
}
const result = await fetchUser('123');
if (!result.ok) {
switch (result.error) {
case 'NOT_FOUND':
return { status: 404, message: 'User not found' };
case 'DB_ERROR':
return { status: 500, message: 'Database error' };
// TypeScript error if you miss a case!
}
}
import { run, type ErrorsOf } from 'awaitly';
const deps = { fetchUser, validateOrder, chargeCard };
type OrderErrors = ErrorsOf<typeof deps>;
const result = await run<{ user: User; order: Order; receipt: Receipt }, OrderErrors>(
async ({ step }) => {
const user = await step('fetchUser', () => fetchUser('123'));
const order = await step('validateOrder', () => validateOrder(orderData));
const receipt = await step('chargeCard', () => chargeCard(user.id, order.total));
return { user, order, receipt };
}
);
// Error type is automatically:
// 'NOT_FOUND' | 'INVALID_ORDER' | 'PAYMENT_FAILED' | UnexpectedError
import { allAsync, allSettledAsync } from 'awaitly';
// Fail-fast: stops on first error
const result = await allAsync([
fetchUser('1'),
fetchUser('2'),
fetchUser('3'),
]);
if (result.ok) {
const [user1, user2, user3] = result.value;
}
// Collect all results (including errors)
const settled = await allSettledAsync([
fetchUser('1'),
fetchUser('2'),
fetchUser('3'),
]);
// Returns a Result with collected outcomes
// See [allSettledAsync](reference/api/) for the exact return shape.
import { mapError } from 'awaitly';
const result = await fetchUser('123');
// Transform error to API response format
const apiResult = mapError(result, (e) => ({
code: e,
message: e === 'NOT_FOUND' ? 'User not found' : 'Database error',
timestamp: Date.now(),
}));
import { createWorkflow } from 'awaitly';
const workflow = createWorkflow('workflow', { fetchData, slowOperation });
const result = await workflow.run(async ({ step, deps }) => {
// Built-in retry with exponential backoff
const data = await step.retry(
'fetchData',
() => deps.fetchData(),
{
attempts: 3,
backoff: 'exponential',
initialDelay: 100,
}
);
// Built-in timeout
const slow = await step.withTimeout(
'slowOp',
() => deps.slowOperation(),
{ ms: 5000 }
);
return { data, slow };
});

You can adopt awaitly incrementally. Here’s how to wrap existing Promise-based code:

import { ok, err, from, fromPromise, type AsyncResult } from 'awaitly';
// Wrap an existing async function manually
const safeFetchUser = async (id: string): AsyncResult<User, 'FETCH_ERROR'> => {
try {
const user = await legacyFetchUser(id); // existing Promise function
return ok(user);
} catch {
return err('FETCH_ERROR');
}
};
// Or use fromPromise to wrap a Promise directly
const safeResult = await fromPromise(
legacyFetchUser('123'),
() => 'FETCH_ERROR' as const
);
// Sync: from(fn, onError) for throwing functions
const safeJsonParse = (s: string) =>
from(
() => JSON.parse(s),
() => 'PARSE_ERROR' as const
);
// Inside workflows: step.try('id', () => …, { error: 'MY_ERROR' })
  • Small scripts — A few async calls, errors handled ad hoc.
  • Simple services — Thin wrappers, single responsibility, errors don’t need to compose.
  • Thin API wrappers — You translate to/from a typed layer at the boundary.
  • Codebases where error types don’t matter — Internal tools, low-risk paths.

Plain Promises with disciplined error handling are perfectly reasonable there.

  • You want compile-time error visibility
  • Building reliable multi-step workflows
  • You need automatic error type inference
  • You want built-in retry/timeout/caching
  • TypeScript type safety is important to your team

A Promise tells you when something finishes. awaitly also tells you what it needs and what it can fail with.

┌────────────────────────────────────────────────────────────────┐
│ Error Visibility Spectrum │
├────────────────────────────────────────────────────────────────┤
│ │
│ Promise + try/catch awaitly AsyncResult │
│ ───────────────────── ──────────────────── │
│ Errors hidden Errors in types │
│ Runtime discovery Compile-time safety │
│ unknown in catch Typed E │
│ │
│ "I hope I caught everything" "TypeScript tells me" │
│ │
└────────────────────────────────────────────────────────────────┘