Skip to content

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.

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 handling

When 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.

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
*/
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 it
const 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' }
*/
// The function signature tells you exactly what can fail
const processPayment = async (
amount: number
): AsyncResult<Receipt, 'INSUFFICIENT_FUNDS' | 'CARD_DECLINED' | 'NETWORK_ERROR'> => {
// ...
};
// TypeScript knows all possible errors
const result = await processPayment(100);
if (!result.ok) {
// Autocomplete shows: 'INSUFFICIENT_FUNDS' | 'CARD_DECLINED' | 'NETWORK_ERROR'
console.log(result.error);
}
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!
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 failures
const [successes, failures] = Awaitly.partition(results);
console.log(`Found ${successes.length} users`);
console.log(`Failed to find ${failures.length} users`);
import { Awaitly } from 'awaitly';
const result = Awaitly.ok(5);
// Transform the value
const doubled = Awaitly.map(result, n => n * 2);
// { ok: true, value: 10 }
// Chain operations that might fail
const validated = Awaitly.andThen(result, n =>
n > 0 ? Awaitly.ok(n) : Awaitly.err('NEGATIVE')
);
  • 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
  • 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.

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 cause
const result3 = Awaitly.from(
() => JSON.parse(data),
(cause) => ({ type: 'PARSE_ERROR' as const, message: String(cause) })
);
// Inside workflows: step.try('id', () => …, { error: 'MY_ERROR' })
Aspecttry/catchResult types
Error visibilityHidden in runtimeExplicit in types
Compiler helpNoneFull type checking
DocumentationSeparate (if any)In function signature
CompositionNested try/catchClean chaining
Partial failureManual handlingBuilt-in (allSettled)
Learning curveFamiliarNew pattern

Takeaway: try/catch hides failure in control flow. awaitly makes failure part of the function’s contract.