Troubleshooting
This guide covers common issues you might encounter when using awaitly and how to resolve them.
Type Errors
Section titled “Type Errors””Type ‘X’ is not assignable to type ‘Y’”
Section titled “”Type ‘X’ is not assignable to type ‘Y’””This usually happens when error types don’t match what the workflow expects.
// Error: Type '"UNKNOWN"' is not assignable to type '"NOT_FOUND" | "EMAIL_FAILED"'const workflow = createWorkflow('workflow', { fetchUser, sendEmail });
const result = await workflow.run(async ({ step, deps }) => { const user = await step('fetchUser', () => deps.fetchUser('1')); await step.try( 'riskyOp', () => riskyOperation(), { error: 'UNKNOWN' } // Not in dependencies! ); return user;});// Option 1: Add the error type to a dependencyasync function riskyOp(): AsyncResult<void, 'UNKNOWN'> { ... }const workflow = createWorkflow('workflow', { fetchUser, sendEmail, riskyOp });
// Option 2: Use run() with explicit error typesconst result = await run<User, 'NOT_FOUND' | 'EMAIL_FAILED' | 'UNKNOWN'>( async ({ step }) => { ... });”Property ‘value’ does not exist on type ‘Result’”
Section titled “”Property ‘value’ does not exist on type ‘Result’””You’re trying to access .value without narrowing the type first.
const result = await workflow.run(async ({ step, deps }) => { ... });console.log(result.value); // Error!const result = await workflow.run(async ({ step, deps }) => { ... });
// Option 1: Check ok firstif (result.ok) { console.log(result.value); // Safe}
// Option 2: Use unwrap helpersimport { unwrap, unwrapOr } from 'awaitly';
const value = unwrap(result); // Throws if errconst value = unwrapOr(result, defaultValue); // Returns default if err”Argument of type ’() => Promise<X>’ is not assignable”
Section titled “”Argument of type ’() => Promise<X>’ is not assignable””Your operation doesn’t return a Result type.
const workflow = createWorkflow('workflow', { });
const result = await workflow.run(async ({ step, deps }) => { // Error: fetch returns Promise<Response>, not Result const response = await step('fetch', () => fetch('/api')); return response;});// Option 1: Use step.try for throwing codeconst result = await workflow.run(async ({ step, deps }) => { const response = await step.try( 'fetchApi', () => fetch('/api').then(r => r.json()), { error: 'FETCH_FAILED' as const } ); return response;});
// Option 2: Wrap in a Result-returning functionasync function fetchApi(): AsyncResult<Data, 'FETCH_FAILED'> { try { const res = await fetch('/api'); return ok(await res.json()); } catch { return err('FETCH_FAILED'); }}Error type is unknown instead of my expected types
Section titled “Error type is unknown instead of my expected types”This happens when TypeScript can’t infer your error types.
// Error type is unknownconst fetchUser = async (id: string) => { const user = await db.find(id); return user ? ok(user) : err('NOT_FOUND');};// Use explicit return type annotationconst fetchUser = async (id: string): AsyncResult<User, 'NOT_FOUND'> => { const user = await db.find(id); return user ? ok(user) : err('NOT_FOUND');};
// Or use 'as const' for literal typesconst fetchUser = async (id: string) => { const user = await db.find(id); return user ? ok(user) : err('NOT_FOUND' as const);};Runtime Errors
Section titled “Runtime Errors””Cannot read property ‘ok’ of undefined”
Section titled “”Cannot read property ‘ok’ of undefined””Your operation returned undefined instead of a Result.
async function fetchUser(id: string) { const user = await db.find(id); if (user) return ok(user); // Missing return statement for error case!}async function fetchUser(id: string): AsyncResult<User, 'NOT_FOUND'> { const user = await db.find(id); if (user) return ok(user); return err('NOT_FOUND'); // Always return a Result}Workflow hangs indefinitely
Section titled “Workflow hangs indefinitely”Usually caused by an unresolved promise or missing await.
- Forgot to
awaitan async step - Operation never resolves or rejects
- Infinite loop in operation
- Deadlock in concurrent operations
// Add timeouts to operationsconst result = await workflow.run(async ({ step, deps }) => { const data = await step.withTimeout( 'slowOp', () => deps.potentiallySlowOp(), { ms: 30000 } ); return data;});
// Use cancellationconst controller = new AbortController();setTimeout(() => controller.abort('timeout'), 60000);
const workflow = createWorkflow('workflow', deps, { signal: controller.signal });”Maximum call stack size exceeded”
Section titled “”Maximum call stack size exceeded””Usually caused by circular dependencies or infinite recursion.
// Problem: Circular dependency between workflowsconst workflowA = createWorkflow('workflow', { workflowB }); // workflowB depends on workflowA!
// Solution: Break the cycleconst workflowA = createWorkflow('workflow', { fetchData });const workflowB = createWorkflow('workflow', { processData });// Call them sequentially, not as dependenciesDebugging Workflows
Section titled “Debugging Workflows”Enable workflow events
Section titled “Enable workflow events”Use the onEvent option to log what’s happening:
const workflow = createWorkflow('workflow', deps, { onEvent: (event) => { switch (event.type) { case 'step_start': console.log(`[START] ${event.name}`); break; case 'step_complete': console.log(`[DONE] ${event.name} (${event.durationMs}ms)`); break; case 'step_error': console.log(`[ERROR] ${event.name}:`, event.error); break; case 'step_retry': console.log(`[RETRY] ${event.name} attempt ${event.attempt}`); break; } },});Use visualization
Section titled “Use visualization”import { createVisualizer } from 'awaitly-visualizer';
const viz = createVisualizer({ workflowName: 'my-workflow' });const workflow = createWorkflow('workflow', deps, { onEvent: viz.handleEvent });
const result = await workflow.run(async ({ step, deps }) => { ... });
// See what happenedconsole.log(viz.renderAs('mermaid'));Visualizer not capturing events (empty or only “workflow_start”)
Section titled “Visualizer not capturing events (empty or only “workflow_start”)”If your diagram is empty or only shows the start node, events are not reaching the visualizer. Ensure:
- You pass
onEvent— The workflow only emits events to a callback you provide. PassonEvent: viz.handleEvent(orcollector.handleEvent) when creating the workflow or when calling the function that runs it. - Library workflows — If you call a function like
processOnePayment(..., deps)that creates the workflow internally, that function must accept optional workflow options (e.g.{ onEvent }) and forward them tocreateWorkflow. Otherwise your visualizer never receives events. See Visualization: Library workflows. - Steps actually run — The workflow must execute at least one
step(...)so thatstep_start/step_completeevents are emitted. If the workflow exits early or throws before any step, you may only seeworkflow_start.
Inspect step results
Section titled “Inspect step results”const workflow = createWorkflow('workflow', deps, { onEvent: (event) => { if (event.type === 'step_complete') { console.log('Step result:', event.result); } },});Performance Issues
Section titled “Performance Issues”Workflow is slow
Section titled “Workflow is slow”Sequential steps that could run in parallel:
const result = await workflow.run(async ({ step, deps }) => { const user = await step('fetchUser', () => deps.fetchUser('1')); const posts = await step('fetchPosts', () => deps.fetchPosts('1')); // Waits for user const comments = await step('fetchComments', () => deps.fetchComments('1')); // Waits for posts return { user, posts, comments };});Run independent operations in parallel:
import { allAsync } from 'awaitly';
const result = await workflow.run(async ({ step, deps }) => { const [user, posts, comments] = await step('fetchUserData', () => allAsync([ deps.fetchUser('1'), deps.fetchPosts('1'), deps.fetchComments('1'), ]) ); return { user, posts, comments };});Memory usage is high
Section titled “Memory usage is high”- Use streaming for large data instead of loading everything
- Clear caches when done
- Use
processInBatchesfor large datasets
import { processInBatches } from 'awaitly/batch';
// Instead of: await Promise.all(users.map(processUser))const result = await processInBatches( users, processUser, { batchSize: 100, concurrency: 5 });Too many API calls
Section titled “Too many API calls”Use singleflight to dedupe concurrent requests:
import { singleflight } from 'awaitly/singleflight';
const fetchUserOnce = singleflight(fetchUser, { key: (id) => `user:${id}`,});
// 10 concurrent calls → 1 actual API callawait Promise.all(ids.map(id => fetchUserOnce(id)));Common Mistakes
Section titled “Common Mistakes”Forgetting as const for error literals
Section titled “Forgetting as const for error literals”// Bad: error type is stringreturn err('NOT_FOUND');
// Good: error type is 'NOT_FOUND'return err('NOT_FOUND' as const);
// Best: explicit return type handles thisasync function fn(): AsyncResult<User, 'NOT_FOUND'> { return err('NOT_FOUND'); // Automatically literal}Using step() outside workflow
Section titled “Using step() outside workflow”// Error: step is only available inside workflow callbackconst user = await step('fetchUser', () => fetchUser('1')); // step is undefined!
// Correct: use inside workflowconst result = await workflow.run(async ({ step, deps }) => { const user = await step('fetchUser', () => deps.fetchUser('1')); return user;});Mixing throw and Result
Section titled “Mixing throw and Result”// Bad: inconsistent error handlingasync function fetchUser(id: string): AsyncResult<User, 'NOT_FOUND'> { const user = await db.find(id); if (!user) throw new Error('Not found'); // Don't throw! return ok(user);}
// Good: always return Resultasync function fetchUser(id: string): AsyncResult<User, 'NOT_FOUND'> { const user = await db.find(id); return user ? ok(user) : err('NOT_FOUND');}Not handling all error cases
Section titled “Not handling all error cases”// TypeScript doesn't enforce handling all errors by defaultconst result = await workflow.run(async ({ step, deps }) => { ... });
if (!result.ok) { // result.error could be 'NOT_FOUND' | 'EMAIL_FAILED' | UnexpectedError // Make sure to handle all cases!}
// Use exhaustive matching with TaggedErrorimport { TaggedError } from 'awaitly';
if (!result.ok) { const message = TaggedError.match(result.error, { NOT_FOUND: () => 'User not found', EMAIL_FAILED: () => 'Email delivery failed', // TypeScript will error if you miss a case });}Returning ok() from workflow executor (double-wrapping)
Section titled “Returning ok() from workflow executor (double-wrapping)”A common mistake: workflow executors should return raw values, not ok().
const result = await workflow.run(async ({ step, deps }) => { const user = await step('fetchUser', () => deps.fetchUser('1')); return ok({ user }); // Wrong! Causes double-wrapping});// result.value is { ok: true, value: { user: ... } }// result.value.user is undefined!const result = await workflow.run(async ({ step, deps }) => { const user = await step('fetchUser', () => deps.fetchUser('1')); return { user }; // Correct: return raw value});// result.value is { user: ... }Mental model:
- step() accepts
Result | AsyncResultand unwraps them - Workflow executors return raw values; awaitly wraps them
Getting Help
Section titled “Getting Help”If you’re still stuck:
- Check the API Reference for correct usage
- Look at example patterns for common scenarios
- Search GitHub Issues for similar problems
- Open a new issue with a minimal reproduction