Skip to content

Troubleshooting

This guide covers common issues you might encounter when using awaitly and how to resolve them.

”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;
});

”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!

”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;
});

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 unknown
const fetchUser = async (id: string) => {
const user = await db.find(id);
return user ? ok(user) : err('NOT_FOUND');
};

”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!
}

Usually caused by an unresolved promise or missing await.

  • Forgot to await an async step
  • Operation never resolves or rejects
  • Infinite loop in operation
  • Deadlock in concurrent operations

Usually caused by circular dependencies or infinite recursion.

// Problem: Circular dependency between workflows
const workflowA = createWorkflow('workflow', { workflowB }); // workflowB depends on workflowA!
// Solution: Break the cycle
const workflowA = createWorkflow('workflow', { fetchData });
const workflowB = createWorkflow('workflow', { processData });
// Call them sequentially, not as dependencies

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;
}
},
});
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 happened
console.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:

  1. You pass onEvent — The workflow only emits events to a callback you provide. Pass onEvent: viz.handleEvent (or collector.handleEvent) when creating the workflow or when calling the function that runs it.
  2. 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 to createWorkflow. Otherwise your visualizer never receives events. See Visualization: Library workflows.
  3. Steps actually run — The workflow must execute at least one step(...) so that step_start / step_complete events are emitted. If the workflow exits early or throws before any step, you may only see workflow_start.
const workflow = createWorkflow('workflow', deps, {
onEvent: (event) => {
if (event.type === 'step_complete') {
console.log('Step result:', event.result);
}
},
});

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 };
});
  • Use streaming for large data instead of loading everything
  • Clear caches when done
  • Use processInBatches for 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 }
);

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 call
await Promise.all(ids.map(id => fetchUserOnce(id)));
// Bad: error type is string
return err('NOT_FOUND');
// Good: error type is 'NOT_FOUND'
return err('NOT_FOUND' as const);
// Best: explicit return type handles this
async function fn(): AsyncResult<User, 'NOT_FOUND'> {
return err('NOT_FOUND'); // Automatically literal
}
// Error: step is only available inside workflow callback
const user = await step('fetchUser', () => fetchUser('1')); // step is undefined!
// Correct: use inside workflow
const result = await workflow.run(async ({ step, deps }) => {
const user = await step('fetchUser', () => deps.fetchUser('1'));
return user;
});
// Bad: inconsistent error handling
async 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 Result
async function fetchUser(id: string): AsyncResult<User, 'NOT_FOUND'> {
const user = await db.find(id);
return user ? ok(user) : err('NOT_FOUND');
}
// TypeScript doesn't enforce handling all errors by default
const 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 TaggedError
import { 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!

Mental model:

  • step() accepts Result | AsyncResult and unwraps them
  • Workflow executors return raw values; awaitly wraps them

If you’re still stuck:

  1. Check the API Reference for correct usage
  2. Look at example patterns for common scenarios
  3. Search GitHub Issues for similar problems
  4. Open a new issue with a minimal reproduction