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.
The Conceptual Difference
Section titled “The Conceptual Difference”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):
- Rejection is unstructured: any value can be thrown. Errors aren’t declared in the type, so
Promise<T>tells you nothing about failure modes. - Composition loses failure information: when you chain or combine promises, failure types widen to
unknown, and you inspect errors at runtime. - 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.
Philosophy Comparison
Section titled “Philosophy Comparison”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 │└─────────────────────────┘ └──────────────────────────┘Quick Comparison
Section titled “Quick Comparison”| 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) |
Basic Usage
Section titled “Basic Usage”Returning Errors
Section titled “Returning Errors”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 typeconst 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 fetchUser = async (id: string): Promise<User> => { const user = await db.find(id); if (!user) { throw new Error('NOT_FOUND'); } return user;};
// Caller has no idea this can failconst user = await fetchUser('123');// ^? Promise<User> -- errors are invisible!
// Must remember to wrap in try/catchtry { const user = await fetchUser('123'); console.log(user.name);} catch (e) { // e is 'unknown' - no type safety console.log(e);}Error Type Safety
Section titled “Error Type Safety”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! }}try { const user = await fetchUser('123');} catch (e) { // e is 'unknown' - you're guessing if (e instanceof Error) { if (e.message === 'NOT_FOUND') { return { status: 404, message: 'User not found' }; } // Did we handle all cases? Who knows! }}Composing Operations
Section titled “Composing Operations”Sequential Operations
Section titled “Sequential Operations”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' | UnexpectedErrorimport { createWorkflow } from 'awaitly';
// createWorkflow infers errors automatically — no ErrorsOf neededconst 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 };});
// Same error type, plus retry/caching/observability when you need itconst processOrder = async () => { try { const user = await fetchUser('123'); const order = await validateOrder(orderData); const receipt = await chargeCard(user.id, order.total); return { user, order, receipt }; } catch (e) { // Which function threw? What kind of error? // You have to manually track this if (e instanceof NotFoundError) { /* ... */ } if (e instanceof ValidationError) { /* ... */ } if (e instanceof PaymentError) { /* ... */ } throw e; // Re-throw unknown errors }};Parallel Operations
Section titled “Parallel Operations”import { allAsync, allSettledAsync } from 'awaitly';
// Fail-fast: stops on first errorconst 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.// Promise.all: fails on first errortry { const [user1, user2, user3] = await Promise.all([ fetchUser('1'), fetchUser('2'), fetchUser('3'), ]);} catch (e) { // One failed, but which one?}
// Promise.allSettled: collects allconst results = await Promise.allSettled([ fetchUser('1'), fetchUser('2'), fetchUser('3'),]);
// Must manually filter and type-checkconst successes = results .filter((r): r is PromiseFulfilledResult<User> => r.status === 'fulfilled') .map(r => r.value);Error Transformation
Section titled “Error Transformation”import { mapError } from 'awaitly';
const result = await fetchUser('123');
// Transform error to API response formatconst apiResult = mapError(result, (e) => ({ code: e, message: e === 'NOT_FOUND' ? 'User not found' : 'Database error', timestamp: Date.now(),}));const fetchUserForApi = async (id: string) => { try { return await fetchUser(id); } catch (e) { // Transform and re-throw throw { code: e instanceof Error ? e.message : 'UNKNOWN', message: 'Something went wrong', timestamp: Date.now(), }; }};Retries and Resilience
Section titled “Retries and Resilience”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 };});// Must implement retry logic yourselfconst withRetry = async <T>( fn: () => Promise<T>, attempts: number, delay: number): Promise<T> => { for (let i = 0; i < attempts; i++) { try { return await fn(); } catch (e) { if (i === attempts - 1) throw e; await new Promise(r => setTimeout(r, delay * Math.pow(2, i))); } } throw new Error('Unreachable');};
// Must implement timeout yourselfconst withTimeout = <T>( fn: () => Promise<T>, ms: number): Promise<T> => { return Promise.race([ fn(), new Promise<never>((_, reject) => setTimeout(() => reject(new Error('Timeout')), ms) ), ]);};
const result = await withRetry(() => withTimeout(fetchData, 5000), 3, 100);Migration Path
Section titled “Migration Path”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 manuallyconst 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 directlyconst safeResult = await fromPromise( legacyFetchUser('123'), () => 'FETCH_ERROR' as const);
// Sync: from(fn, onError) for throwing functionsconst safeJsonParse = (s: string) => from( () => JSON.parse(s), () => 'PARSE_ERROR' as const );
// Inside workflows: step.try('id', () => …, { error: 'MY_ERROR' })When to Use Each
Section titled “When to Use Each”When Promises are enough
Section titled “When Promises are enough”- 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.
Use awaitly when:
Section titled “Use awaitly when:”- 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.
Summary
Section titled “Summary”┌────────────────────────────────────────────────────────────────┐│ 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" ││ │└────────────────────────────────────────────────────────────────┘