Policies
Policies control retries, timeouts, and fallbacks. awaitly offers two patterns:
- Per-dep policies (recommended): declare
retry,timeout, andfallbackwrappers in the deps object. Call sites stay clean, and the analyzer reads policy chains from the deps literal. - StepOptions bundles (legacy): apply
withPolicyorservicePoliciesper step through options.
Per-dep policies
Section titled “Per-dep policies”Wrap dependencies at declaration site. Policies compose inside-out. retry(timeout(fn, 5000), { attempts: 3 }) applies the timeout before each retry.
import { ok, run, retry, timeout, fallback, tryAsync, createWorkflow } from 'awaitly';
// Deps are *functions* — policies wrap them, so `tryAsync` goes inside.const charge = (amount: number) => tryAsync( () => paymentGateway.charge(amount), (cause) => ({ type: 'CHARGE_FAILED', cause }) );
const sendEmail = (to: string) => tryAsync( () => emailService.send(to), (cause) => ({ type: 'SEND_FAILED', cause }) );
// Standalone composition with run()const result = await run( { charge: retry(timeout(charge, 5000), { attempts: 3 }), notify: fallback(sendEmail, () => ok(undefined)), }, async (s) => { await s.charge(amount); await s.notify(to); return ok(undefined); });
// Same wrappers in a workflow deps objectconst checkout = createWorkflow('checkout', { charge: retry(timeout(charge, 5000), { attempts: 3 }), notify: fallback(sendEmail, () => ok(undefined)),});Error-union behavior
Section titled “Error-union behavior”| Wrapper | Effect on error union |
|---|---|
retry(fn, opts) |
Preserves the base union. The last failure propagates. |
timeout(fn, ms) |
Adds TimeoutError to the union |
fallback(fn, handler) |
Consumes base errors; only handler errors remain |
Plain (non-Result) functions are valid inputs: return values normalize to ok(), throws surface as UnexpectedError at the run/workflow layer. Wrappers preserve the base function’s name so events and diagrams keep showing the dep name.
By default, retry skips UnexpectedError and untagged throws. Typed errors still retry. Pass retryIf: () => true to opt back into retrying throws.
Pass clock into retry(fn, { clock }) and timeout(fn, after, { clock }) at wrap time. Policy wrappers do not pick up run({ clock }) automatically. Use step.retry / step.withTimeout / step.sleep inside a run that has clock, or pass the same clock when wrapping. Tests use createTestClock() from awaitly/testing. See Retries & Timeouts: testing with a clock.
See Result types: policies and Retries & Timeouts: deps-level policies.
StepOptions bundles (legacy)
Section titled “StepOptions bundles (legacy)”Reusable bundles of StepOptions (retry, timeout, cache keys) applied per step via withPolicy and related helpers.
Using service policies
Section titled “Using service policies”Apply pre-built policies for common scenarios:
import { withPolicy, servicePolicies } from 'awaitly';
const result = await workflow.run(async ({ step }) => { // HTTP API: 5s timeout, 3 retries const user = await step( 'fetch-user', () => fetchUser(id), withPolicy(servicePolicies.httpApi) );
// Database: 30s timeout, 2 retries const orders = await step( 'fetch-orders', () => db.query('SELECT * FROM orders'), withPolicy(servicePolicies.database) );
// Cache: 1s timeout, no retry const cached = await step( 'cache-lookup', () => cache.get(key), withPolicy(servicePolicies.cache) );
return { user, orders, cached };});Combining policies
Section titled “Combining policies”Merge multiple policies together:
import { withPolicies, timeoutPolicies, retryPolicies } from 'awaitly';
const data = await step( 'fetch-data', () => fetchData(), withPolicies([timeoutPolicies.api, retryPolicies.standard]));Policy applier
Section titled “Policy applier”Create a reusable applier for consistent defaults:
import { createPolicyApplier, timeoutPolicies, retryPolicies } from 'awaitly';
const applyPolicy = createPolicyApplier( timeoutPolicies.api, retryPolicies.transient);
const result = await step( 'api-call', () => callApi(), applyPolicy({ key: 'cache:api' }));Fluent builder
Section titled “Fluent builder”Build step options with a fluent API:
import { stepOptions } from 'awaitly';
const options = stepOptions() .key('user:123') .timeout(5000) .retries(3) .build();
const user = await step('fetchUser', () => fetchUser('123'), options);Policy registry
Section titled “Policy registry”Create organization-wide policy standards:
import { createPolicyRegistry, servicePolicies } from 'awaitly';
const registry = createPolicyRegistry();registry.register('api', servicePolicies.httpApi);registry.register('db', servicePolicies.database);registry.register('cache', servicePolicies.cache);registry.register('queue', servicePolicies.messageQueue);
// Use in workflowsconst user = await step( 'fetch-user', () => fetchUser(id), registry.apply('api'));
const data = await step( 'query-data', () => db.query(sql), registry.apply('db'));Available presets
Section titled “Available presets”Retry policies
Section titled “Retry policies”import { retryPolicies } from 'awaitly';
retryPolicies.none // No retryretryPolicies.transient // 3 attempts, fast backoffretryPolicies.standard // 3 attempts, moderate backoffretryPolicies.aggressive // 5 attempts, longer backoffretryPolicies.fixed(3, 1000) // 3 attempts, 1s fixed delayretryPolicies.linear(3, 100) // 3 attempts, linear backoffTimeout policies
Section titled “Timeout policies”import { timeoutPolicies } from 'awaitly';
timeoutPolicies.fast // 1 secondtimeoutPolicies.api // 5 secondstimeoutPolicies.extended // 30 secondstimeoutPolicies.long // 2 minutestimeoutPolicies.ms(3000) // Custom millisecondsService policies
Section titled “Service policies”Combined retry + timeout for specific scenarios:
import { servicePolicies } from 'awaitly';
servicePolicies.httpApi // 5s timeout, 3 retriesservicePolicies.database // 30s timeout, 2 retriesservicePolicies.cache // 1s timeout, no retryservicePolicies.messageQueue // 30s timeout, 5 retriesservicePolicies.fileSystem // 2min timeout, 3 retriesservicePolicies.rateLimited // 10s timeout, 5 linear retriesCustom policies
Section titled “Custom policies”Create your own policies:
import { mergePolicies } from 'awaitly';
const myApiPolicy = { timeout: { ms: 10000 }, retry: { attempts: 4, backoff: 'exponential', initialDelay: 200, maxDelay: 5000, },};
const myDbPolicy = { timeout: { ms: 60000 }, retry: { attempts: 2, backoff: 'fixed', initialDelay: 1000, },};
// Combine with existing policiesconst criticalApiPolicy = mergePolicies( servicePolicies.httpApi, { retry: { attempts: 5 } });When to use policies
Section titled “When to use policies”| Scenario | Recommended Policy |
|---|---|
| External HTTP APIs | servicePolicies.httpApi |
| Database queries | servicePolicies.database |
| Cache operations | servicePolicies.cache |
| Message queue consumers | servicePolicies.messageQueue |
| File system operations | servicePolicies.fileSystem |
| Rate-limited APIs | servicePolicies.rateLimited |
Testing policies
Section titled “Testing policies”Test policy configuration
Section titled “Test policy configuration”import { describe, it, expect } from 'vitest';import { servicePolicies, mergePolicies } from 'awaitly';
describe('custom policies', () => { it('merges correctly with base policies', () => { const customApi = mergePolicies( servicePolicies.httpApi, { retry: { attempts: 5 } } );
expect(customApi.retry?.attempts).toBe(5); expect(customApi.timeout?.ms).toBe(5000); // Inherited from httpApi });
it('has expected timeout for critical operations', () => { const paymentPolicy = myPolicies.payment;
// Payment should have longer timeout expect(paymentPolicy.timeout?.ms).toBeGreaterThanOrEqual(30000); // And more retries expect(paymentPolicy.retry?.attempts).toBeGreaterThanOrEqual(3); });});Test policy application in workflows
Section titled “Test policy application in workflows”import { createWorkflowHarness, createMockFn } from 'awaitly/testing';import { ok, err, withPolicy, servicePolicies, type AsyncResult } from 'awaitly';
describe('workflow with policies', () => { it('retries on transient failure', async () => { // `returns`/`returnsOnce` take Results — `okOutcome`/`errOutcome` are for // `harness.script()` / `harness.scriptStep()`. const mockFetch = createMockFn<{ id: string }, 'NETWORK_ERROR'>(); mockFetch .returnsOnce(err('NETWORK_ERROR')) .returnsOnce(err('NETWORK_ERROR')) .returns(ok({ id: '1' }));
const harness = createWorkflowHarness({ fetchData: mockFetch });
const result = await harness.run(async ({ step, deps }) => { return await step( 'fetch', () => deps.fetchData(), withPolicy(servicePolicies.httpApi) ); });
expect(result.ok).toBe(true); expect(mockFetch.getCallCount()).toBe(3); // Retried twice });
it('times out slow operations', async () => { const slowFetch = (): AsyncResult<{ data: string }, never> => new Promise((resolve) => setTimeout(() => resolve(ok({ data: 'slow' })), 10000) );
const harness = createWorkflowHarness({ fetchData: slowFetch });
const result = await harness.run(async ({ step, deps }) => { return await step( 'fetch', () => deps.fetchData(), withPolicy({ timeout: { ms: 100 } }) ); });
expect(result.ok).toBe(false); // Should timeout, not succeed });});Domain-specific policy patterns
Section titled “Domain-specific policy patterns”E-commerce policies
Section titled “E-commerce policies”const ecommercePolicies = { // Payment processing - high reliability needed payment: mergePolicies(servicePolicies.httpApi, { timeout: { ms: 30000 }, retry: { attempts: 3, backoff: 'exponential', initialDelay: 1000 }, }),
// Inventory check - can fail fast inventory: mergePolicies(servicePolicies.httpApi, { timeout: { ms: 2000 }, retry: { attempts: 1 }, }),
// Order database - needs durability orderDb: mergePolicies(servicePolicies.database, { timeout: { ms: 60000 }, retry: { attempts: 3 }, }),
// Email notifications - best effort notifications: { timeout: { ms: 5000 }, retry: { attempts: 1 }, },};
// Register globallyconst registry = createPolicyRegistry();Object.entries(ecommercePolicies).forEach(([name, policy]) => { registry.register(name, policy);});Microservices policies
Section titled “Microservices policies”const microservicesPolicies = { // Internal services - trusted, fast internal: { timeout: { ms: 2000 }, retry: { attempts: 2, backoff: 'fixed', initialDelay: 100 }, },
// External APIs - less trusted, slower external: { timeout: { ms: 10000 }, retry: { attempts: 3, backoff: 'exponential', initialDelay: 500 }, },
// Event publishing - fire and forget with retry events: { timeout: { ms: 5000 }, retry: { attempts: 5, backoff: 'linear', initialDelay: 200 }, },
// Cache operations - fail fast cache: { timeout: { ms: 500 }, retry: { attempts: 0 }, },};Conditional policy selection
Section titled “Conditional policy selection”function getPolicyForService(service: string, criticality: 'low' | 'medium' | 'high') { const basePolicy = servicePolicies.httpApi;
const criticalityModifiers = { low: { retry: { attempts: 1 }, timeout: { ms: 2000 } }, medium: { retry: { attempts: 3 }, timeout: { ms: 5000 } }, high: { retry: { attempts: 5 }, timeout: { ms: 30000 } }, };
return mergePolicies(basePolicy, criticalityModifiers[criticality]);}
// Usageconst paymentPolicy = getPolicyForService('stripe', 'high');const analyticsPolicy = getPolicyForService('mixpanel', 'low');Best practices
Section titled “Best practices”- Use policies consistently - Same service type should use same policy
- Register in one place - Use
createPolicyRegistryfor organization-wide standards - Don’t over-configure - Presets handle most cases well
- Adjust for criticality - Payment APIs may need more retries than logging
- Monitor and tune - Adjust based on actual failure patterns
- Test your policies - Verify retry and timeout behavior in tests
- Document policy decisions - Explain why each service has its configuration