awaitly vs try/catch
Traditional JavaScript uses try/catch for error handling. awaitly uses Result types that make errors explicit in the type system. This guide shows the same operations in both styles.
The Conceptual Difference
Section titled “The Conceptual Difference”try/catch models failure as control flow. A function that throws has a signature that lies: it says Promise<User>, but it might throw six different things. The caller has no idea what can go wrong.
awaitly models failure as part of the function’s type. The signature says what can fail. Errors compose. Callers are nudged (or forced) to handle them. Everything else in this guide is a consequence of that.
try/catch Result types───────────────── ─────────────────• Errors are implicit • Errors are explicit• Signature hides failure • Signature is the contract• Caught at runtime • Caught at compile time• Easy to forget handling • TypeScript enforces handlingWhen try/catch is fine: Small scopes, scripts, internal or low-risk apps. Plain async/await with disciplined error handling is perfectly reasonable there.
Where it breaks down: When errors become part of your domain, multiple layers need to coordinate, or you want refactor safety. awaitly addresses that by encoding failure in types, making composition predictable, and keeping dependencies explicit.
Simple Operations
Section titled “Simple Operations”Parsing JSON
Section titled “Parsing JSON”import { Awaitly } from 'awaitly';
const parseJson = (str: string) => Awaitly.from(() => JSON.parse(str));
const result = parseJson('{"name": "Alice"}');
if (result.ok) { console.log(result.value.name);} else { console.log('Parse error:', result.error.message);}/*Output:Alice*/const parseJson = (str: string) => { try { return JSON.parse(str); } catch (error) { console.log('Parse error:', error.message); return null; }};
const result = parseJson('{"name": "Alice"}');
if (result !== null) { console.log(result.name);}/*Output:Alice*/API Calls
Section titled “API Calls”import { Awaitly, type AsyncResult } from 'awaitly';
type FetchError = 'NETWORK_ERROR' | 'NOT_FOUND' | 'SERVER_ERROR';
const fetchUser = async (id: string): AsyncResult<User, FetchError> => { try { const res = await fetch(`/api/users/${id}`); if (!res.ok) { if (res.status === 404) return Awaitly.err('NOT_FOUND'); return Awaitly.err('SERVER_ERROR'); } return Awaitly.ok(await res.json()); } catch { return Awaitly.err('NETWORK_ERROR'); }};
// Caller MUST handle the error - TypeScript enforces itconst result = await fetchUser('123');if (!result.ok) { switch (result.error) { case 'NOT_FOUND': console.log('User not found'); break; case 'NETWORK_ERROR': console.log('Check your connection'); break; case 'SERVER_ERROR': console.log('Try again later'); break; }}/*Output (on success):{ ok: true, value: { id: '123', name: 'Alice' } }
Output (on 404):{ ok: false, error: 'NOT_FOUND' }*/const fetchUser = async (id: string): Promise<User> => { const res = await fetch(`/api/users/${id}`); if (!res.ok) { if (res.status === 404) throw new Error('NOT_FOUND'); throw new Error('SERVER_ERROR'); } return res.json();};
// Caller might forget to wrap in try/catch - no TypeScript warning!try { const user = await fetchUser('123'); console.log(user.name);} catch (error) { // What errors can this throw? TypeScript doesn't know. if (error.message === 'NOT_FOUND') { console.log('User not found'); } else { console.log('Something went wrong'); }}Error Type Safety
Section titled “Error Type Safety”What can go wrong?
Section titled “What can go wrong?”// The function signature tells you exactly what can failconst processPayment = async ( amount: number): AsyncResult<Receipt, 'INSUFFICIENT_FUNDS' | 'CARD_DECLINED' | 'NETWORK_ERROR'> => { // ...};
// TypeScript knows all possible errorsconst result = await processPayment(100);if (!result.ok) { // Autocomplete shows: 'INSUFFICIENT_FUNDS' | 'CARD_DECLINED' | 'NETWORK_ERROR' console.log(result.error);}// No indication in the type what errors can occurconst processPayment = async (amount: number): Promise<Receipt> => { // ...};
// You have to read the implementation or docs to know what can throwtry { const receipt = await processPayment(100);} catch (error) { // What type is error? unknown // What values can it have? Who knows}Chaining Operations
Section titled “Chaining Operations”Sequential operations with early exit
Section titled “Sequential operations with early exit”import { createWorkflow } from 'awaitly/workflow';
const workflow = createWorkflow('workflow', { fetchUser, validateAge, createAccount });
const result = await workflow.run(async ({ step, deps }) => { const user = await step('fetchUser', () => deps.fetchUser('123')); // If fetchUser fails, we stop here and return the error
const validated = await step('validateAge', () => deps.validateAge(user)); // If validateAge fails, we stop here
const account = await step('createAccount', () => deps.createAccount(validated)); // If createAccount fails, we stop here
return account;});
// Error is: 'NOT_FOUND' | 'UNDERAGE' | 'DUPLICATE_EMAIL' | UnexpectedError// TypeScript inferred this automatically!const createUserAccount = async (userId: string) => { try { const user = await fetchUser(userId); // If fetchUser throws, we jump to catch
const validated = await validateAge(user); // If validateAge throws, we jump to catch
const account = await createAccount(validated); // If createAccount throws, we jump to catch
return account; } catch (error) { // What type of error is this? // Which function threw it? // How do we handle each case? throw error; // Often just re-throw because we don't know }};Partial Success
Section titled “Partial Success”When you want all results, even failures
Section titled “When you want all results, even failures”import { Awaitly } from 'awaitly';
const results = await Promise.all([ fetchUser('1'), fetchUser('2'), fetchUser('3'),]);
// Collect all outcomes (successes and failures)const settled = Awaitly.allSettled(results);// See [Awaitly.allSettled](/reference/api/) for the exact return shape.
// Partition into successes and failuresconst [successes, failures] = Awaitly.partition(results);console.log(`Found ${successes.length} users`);console.log(`Failed to find ${failures.length} users`);const results = await Promise.allSettled([ fetchUser('1'), fetchUser('2'), fetchUser('3'),]);
// Have to manually filter and extract valuesconst successes = results .filter(r => r.status === 'fulfilled') .map(r => r.value);const failures = results .filter(r => r.status === 'rejected') .map(r => r.reason);
console.log(`Found ${successes.length} users`);console.log(`Failed to find ${failures.length} users`);Transforming Results
Section titled “Transforming Results”Mapping success values
Section titled “Mapping success values”import { Awaitly } from 'awaitly';
const result = Awaitly.ok(5);
// Transform the valueconst doubled = Awaitly.map(result, n => n * 2);// { ok: true, value: 10 }
// Chain operations that might failconst validated = Awaitly.andThen(result, n => n > 0 ? Awaitly.ok(n) : Awaitly.err('NEGATIVE'));const result = 5;
// Transform the value - but no error handling built inconst doubled = result * 2;
// Chain operations that might fail - need manual try/catchtry { const validated = result > 0 ? result : (() => { throw 'NEGATIVE'; })();} catch (e) { // ...}When to Use Each
Section titled “When to Use Each”Use try/catch when:
Section titled “Use try/catch when:”- Working with legacy code that throws
- Wrapping third-party libraries (then convert to Result)
- Truly exceptional conditions (out of memory, etc.)
- Quick scripts where type safety isn’t critical
- Your app is small, internal, or low-risk — plain async/await with disciplined error handling is perfectly reasonable
Use Result types when:
Section titled “Use Result types when:”- Building new APIs or functions
- Errors are part of normal flow (validation, not found, etc.)
- You want TypeScript to track error types
- You need to compose multiple fallible operations
- You want self-documenting error handling
Bottom line: try/catch hides failure in control flow; awaitly makes failure part of the function’s contract.
Wrapping Throwing Code
Section titled “Wrapping Throwing Code”You don’t have to rewrite everything. Wrap existing code:
import { Awaitly } from 'awaitly';
// Sync: Awaitly.from(fn, onError)const result1 = Awaitly.from(() => JSON.parse(jsonString), () => 'PARSE_ERROR' as const);
// Async (outside workflows): Awaitly.fromPromise(promise, onError)const result2 = await Awaitly.fromPromise( fetch('/api/data').then(r => r.json()), () => 'FETCH_ERROR' as const);
// With error context from the causeconst result3 = Awaitly.from( () => JSON.parse(data), (cause) => ({ type: 'PARSE_ERROR' as const, message: String(cause) }));
// Inside workflows: step.try('id', () => …, { error: 'MY_ERROR' })Summary
Section titled “Summary”| Aspect | try/catch | Result types |
|---|---|---|
| Error visibility | Hidden in runtime | Explicit in types |
| Compiler help | None | Full type checking |
| Documentation | Separate (if any) | In function signature |
| Composition | Nested try/catch | Clean chaining |
| Partial failure | Manual handling | Built-in (allSettled) |
| Learning curve | Familiar | New pattern |
Takeaway: try/catch hides failure in control flow. awaitly makes failure part of the function’s contract.