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: “This async computation will eventually produce a value or reject.” It’s about time — when something finishes.

awaitly models: “This computation may depend on an environment, may fail with a typed error, may perform async work, and composes predictably.” It’s about effects — what it needs, what it may fail with, and what it produces.

One sentence: Promises model eventual values; awaitly models effectful computation.

Where Promises are incomplete (not “wrong”):

  1. Rejection is unstructured — Any value can be thrown. Errors aren’t declared in the type. Promise<T> tells you nothing about failure modes.
  2. Composition loses failure information — When you chain or combine promises, failure types widen to unknown; you must 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 invisibility becomes a scaling problem in larger apps.

awaitly doesn’t replace Promises — it’s a superset abstraction for application-level computation. Promises remain the foundation; awaitly adds a second typed channel (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 │
└─────────────────────────┘ └──────────────────────────┘
FeaturePromise + try/catchawaitly AsyncResult
Error visibilityHidden in runtimeIn return type
Type safetyPromise<T>AsyncResult<T, E>
Error typeunknown in catchTyped E
Compositiontry/catch nestingall, andThen, pipe
Learning curveFamiliarLow (uses async/await)
import { Awaitly, type AsyncResult } from 'awaitly';
const fetchUser = async (id: string): AsyncResult<User, 'NOT_FOUND' | 'DB_ERROR'> => {
try {
const user = await db.find(id);
return user ? Awaitly.ok(user) : Awaitly.err('NOT_FOUND');
} catch {
return Awaitly.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 { createWorkflow } from 'awaitly/workflow';
const workflow = createWorkflow('workflow', { fetchUser, validateOrder, chargeCard });
const result = await workflow.run(async ({ step, deps }) => {
const user = await step('fetchUser', () => deps.fetchUser('123'));
const order = await step('validateOrder', () => deps.validateOrder(orderData));
const receipt = await step('chargeCard', () => deps.chargeCard(user.id, order.total));
return { user, order, receipt };
});
// Error type is automatically:
// 'NOT_FOUND' | 'INVALID_ORDER' | 'PAYMENT_FAILED' | UnexpectedError
if (!result.ok) {
// Handle specific error
}
import { Awaitly } from 'awaitly';
// Fail-fast: stops on first error
const result = await Awaitly.allAsync([
fetchUser('1'),
fetchUser('2'),
fetchUser('3'),
]);
if (result.ok) {
const [user1, user2, user3] = result.value;
}
// Collect all results (including errors)
const settled = await Awaitly.allSettledAsync([
fetchUser('1'),
fetchUser('2'),
fetchUser('3'),
]);
// Returns a Result with collected outcomes
// See [Awaitly.allSettledAsync](/reference/api/) for the exact return shape.
import { Awaitly } from 'awaitly';
const result = await fetchUser('123');
// Transform error to API response format
const apiResult = Awaitly.mapError(result, (e) => ({
code: e,
message: e === 'NOT_FOUND' ? 'User not found' : 'Database error',
timestamp: Date.now(),
}));
import { createWorkflow } from 'awaitly/workflow';
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',
delayMs: 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 { Awaitly, 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 Awaitly.ok(user);
} catch {
return Awaitly.err('FETCH_ERROR');
}
};
// Or use Awaitly.fromPromise to wrap a Promise directly
const safeResult = await Awaitly.fromPromise(
legacyFetchUser('123'),
() => 'FETCH_ERROR' as const
);
// Sync: Awaitly.from(fn, onError) for throwing functions
const safeJsonParse = (s: string) =>
Awaitly.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. That builds credibility.

  • 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

Bottom line: A Promise tells you when something finishes. awaitly tells you what it needs, what it may fail with, and what it produces.

┌────────────────────────────────────────────────────────────────┐
│ 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" │
│ │
└────────────────────────────────────────────────────────────────┘

Takeaway: Promises model eventual values. awaitly models effectful computation.