Errors as Values V2
Last week, I wrote about returning errors as values in TypeScript: Errors as Values. A few days later, I saw a tweet on the same topic by @mattpocockuk, and it made me rethink my approach.
There is actually a better way to handle errors in TypeScript, one that improves both readability and maintainability. In this post, I will walk through how to simplify error handling with helper functions.
The downside of the previous approach was that you had to manually define union return types for every function. It worked, but got repetitive and hard to scale. Instead, we can introduce small helpers like Success()
and Failure()
to standardize our return values.
Here’s the idea: instead of returning a manually typed union, you return either a Success<T>
or a Failure<T>
object. These helper functions make your intent clear and keep your return types consistent across your codebase.
const getData = async () => {
const res = await fetch("some-url.com");
if (!res.ok) return Failure("Failed to fetch data");
const data = await res.json();
return Success<number>(data);
}
const result = await getData();
Define them once, then reuse them everywhere. Success()
for success, Failure()
for errors. TypeScript can now infer your return types automatically.
type Success<T> = {
success: true;
value: T;
}
type Failure<T> = {
success: false;
error: T;
}
const Success = <T>(value: T): Success<T> => ({
success: true,
value,
});
const Failure = <T>(error: T): Failure<T> => ({
success: false,
error,
});
Once you have that setup, your function usage gets much simpler. You check the success
field and either use the value or handle the error.
// ...
const result = await getData();
if (result.success) console.log(result.value);
else console.error(result.error);
This pattern saves time, reduces boilerplate, and improves the developer experience when writing and reading error handling logic.