Skip to content

Handling Errors

When you create a workflow, TypeScript computes the error union from your dependencies:

const fetchUser = async (id: string): AsyncResult<User, 'NOT_FOUND'> => { ... };
const fetchPosts = async (id: string): AsyncResult<Post[], 'FETCH_ERROR'> => { ... };
const sendEmail = async (to: string): AsyncResult<void, 'EMAIL_FAILED'> => { ... };
const workflow = createWorkflow('workflow', { fetchUser, fetchPosts, sendEmail });
const result = await workflow.run(async ({ step, deps }) => { ... });
// result.error is: 'NOT_FOUND' | 'FETCH_ERROR' | 'EMAIL_FAILED' | UnexpectedError

Add a new dependency? The error union updates automatically.

If code throws an exception (not a returned error), it becomes an UnexpectedError (a TaggedError). The original thrown value is in error.cause:

import { isUnexpectedError } from 'awaitly';
const badOperation = async (): AsyncResult<string, 'KNOWN_ERROR'> => {
throw new Error('Something broke'); // Throws instead of returning err()
};
const workflow = createWorkflow('workflow', { badOperation });
const result = await workflow.run(async ({ step, deps }) => {
return await step('badOperation', () => badOperation());
});
if (!result.ok && isUnexpectedError(result.error)) {
console.log(result.error.cause); // The original Error object
}

Use step.try to convert thrown exceptions into typed errors:

const result = await workflow.run(async ({ step, deps }) => {
const data = await step.try(
'fetchData',
async () => {
const res = await fetch('/api/data');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
},
{ error: 'FETCH_FAILED' as const }
);
return data;
});
// result.error includes 'FETCH_FAILED'

If your function returns rich error objects, use step.fromResult:

type ApiError = { code: string; message: string };
const callApi = async (): AsyncResult<Data, ApiError> => {
return err({ code: 'RATE_LIMITED', message: 'Too many requests' });
};
const result = await workflow.run(async ({ step, deps }) => {
const data = await step.fromResult(
'callApi',
() => callApi(),
{
onError: (apiError) => ({
type: 'API_ERROR' as const,
code: apiError.code,
message: apiError.message,
}),
}
);
return data;
});

Use a switch statement for exhaustive handling:

if (!result.ok) {
switch (result.error) {
case 'NOT_FOUND':
return res.status(404).json({ error: 'User not found' });
case 'UNAUTHORIZED':
return res.status(401).json({ error: 'Please log in' });
case 'FETCH_ERROR':
return res.status(502).json({ error: 'Upstream service failed' });
default:
// UnexpectedError or unknown error
console.error(result.error);
return res.status(500).json({ error: 'Internal error' });
}
}

TypeScript ensures you handle all known error cases.

By default, thrown exceptions become an UnexpectedError. With run(), use ErrorOf/Errors to derive error types from your deps:

import { type Errors } from 'awaitly';
type RunErrors = Errors<[typeof fetchUser, typeof fetchPosts]>;
const result = await run<User, RunErrors>(async ({ step }) => {
const user = await step('fetchUser', () => fetchUser('1'));
const posts = await step('fetchPosts', () => fetchPosts(user.id));
return user;
});
// result.error is: 'NOT_FOUND' | 'FETCH_ERROR' | UnexpectedError

To replace UnexpectedError with a custom type, pass catchUnexpected:

const workflow = createWorkflow('workflow', { fetchUser, fetchPosts },
{
catchUnexpected: (thrown) => ({
type: 'UNEXPECTED' as const,
message: String(thrown),
}),
}
);
// result.error is now your workflow errors | { type: 'UNEXPECTED', message: string }
Use caseRecommendation
Simple distinct statesString literals: 'NOT_FOUND' | 'UNAUTHORIZED'
Errors with contextObjects: { type: 'NOT_FOUND', id: string }
API responsesTagged Errors for structured data

Having issues with TypeScript narrowing or error handling? See Troubleshooting.

Learn about Results in depth →