Skip to content

Handling Errors

What TypeScript gives you back covered the error union and where it comes from. This page is about the two practical jobs left: getting throwing code into the union, and acting on the union at your application edge.

Most existing code throws. step.try turns a throw into a typed error:

const result = await workflow.run(async ({ step }) => {
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' }
);
return data;
});
// result.error now includes 'FETCH_FAILED' instead of UnexpectedError

Without step.try, that throw would still be caught. It would arrive as an opaque UnexpectedError rather than something you can switch on.

When an operation already returns a rich error object, step.fromResult lets you reshape it as it enters the workflow:

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

One if, then one switch. This is the only place in your app that deals with failure:

import { isUnexpectedError } from 'awaitly';
if (result.ok) {
return res.status(200).json(result.value);
}
if (isUnexpectedError(result.error)) {
logger.error({ cause: result.error.cause }, 'unhandled exception');
return res.status(500).json({ error: 'Internal error' });
}
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 failed' });
}

Handle UnexpectedError first and the remaining switch is over your own domain errors only, which is what makes an exhaustiveness check meaningful.

Situation Shape
A handful of distinct states String literals: 'NOT_FOUND' | 'UNAUTHORIZED'
The error carries data Object: { type: 'NOT_FOUND', id: string }
Shared across a codebase or API Tagged Errors

Start with string literals. Move to objects when you find yourself needing the id that failed, and to TaggedError when the same error crosses module boundaries.

TypeScript not narrowing the way you expect? See Troubleshooting.

You now know everything needed to use awaitly day to day. Foundations goes deeper on each piece, Result combinators, step options, control flow, retries, persistence, and streaming.

Continue to Foundations →