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: “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”):
- Rejection is unstructured — Any value can be thrown. Errors aren’t declared in the type.
Promise<T>tells you nothing about failure modes. - Composition loses failure information — When you chain or combine promises, failure types widen to
unknown; you must inspect errors at runtime. - 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.
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 { 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 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 { 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' | UnexpectedErrorif (!result.ok) { // Handle specific error}const 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 { Awaitly } from 'awaitly';
// Fail-fast: stops on first errorconst 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.// 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 { Awaitly } from 'awaitly';
const result = await fetchUser('123');
// Transform error to API response formatconst apiResult = Awaitly.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/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 };});// 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 { Awaitly, 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 Awaitly.ok(user); } catch { return Awaitly.err('FETCH_ERROR'); }};
// Or use Awaitly.fromPromise to wrap a Promise directlyconst safeResult = await Awaitly.fromPromise( legacyFetchUser('123'), () => 'FETCH_ERROR' as const);
// Sync: Awaitly.from(fn, onError) for throwing functionsconst safeJsonParse = (s: string) => Awaitly.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. That builds credibility.
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
Bottom line: A Promise tells you when something finishes. awaitly tells you what it needs, what it may fail with, and what it produces.
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" ││ │└────────────────────────────────────────────────────────────────┘Takeaway: Promises model eventual values. awaitly models effectful computation.