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

err() takes a const type parameter, so error literals keep their literal type on their own, no as const required:

const fetchUser = async (id: string) => {
const user = await db.find(id);
return user ? ok(user) : err('NOT_FOUND');
};
// error type is 'NOT_FOUND', not string

Annotating the return type is still worth doing when you want one named error union that the compiler checks every branch against:

const fetchUser = async (id: string): AsyncResult<User, 'NOT_FOUND'> => {
const user = await db.find(id);
return user ? ok(user) : err('NOT_FOUND');
};

Annotating also keeps inferred error objects free of readonly modifiers, which matters if an error carries an array you later assign to a mutable type.

“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

awaitly reports an UnexpectedError when a dependency throws. The original error and its stack frames remain on cause:

async function readConfig(path: string) {
JSON.parse(await fs.readFile(path, 'utf8')); // throws
return ok(path);
}
const result = await run({ readConfig }, async (s) => s.readConfig('/etc/app'));
if (!result.ok) {
console.log(result.error.type); // 'UnexpectedError'
console.log(result.error.cause.stack);
}
SyntaxError: Expected property name or '}' in JSON at position 2
at JSON.parse (<anonymous>)
at readConfig (/app/src/config.ts:4:8)
at .../awaitly/dist/index.js:911:31

The top frames identify the line in your code that threw. awaitly runs steps with await, so the JavaScript engine builds the same stack it builds for other async functions. Your debugger can step through those frames.

Log result.error.cause for failures you did not model. Inspect result.error.type to identify the workflow failure and cause.stack to find the throw site.

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 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';
// 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';
const fetchUserOnce = singleflight(fetchUser, {
key: (id) => `user:${id}`,
});
// 10 concurrent calls → 1 actual API call
await Promise.all(ids.map(id => fetchUserOnce(id)));
// 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