Errors as Values in TypeScript
Contrary to the popular opinion, I’m actually a big fan of Go’s error handling. It makes error handling explicit while keeping the happy path clean.
Examples
In the examples below, example-2.ts just reads much cleaner to me.
try { const data = await fetchData(); return console.log("Data:", data);} catch (error) { if (error instanceof NetworkFailedError) return console.error("Network error:", error); if (error instanceof ValidationFailedError) return console.error("Validation error:", error); throw error;}const data = await fetchData();if (data instanceof NetworkFailedError) return console.error("Network error:", data);if (data instanceof ValidationFailedError) return console.error("Validation error:", data);return console.log("Data:", data);It prevents deeper nesting and makes the return-types consistent. If we e.g. did not yet cover RateLimitError, data will be of type Data | RateLimitError.
Zero-Dependency Philosophy
The Zero-Dependency Philosophy advocated by Tommy D. Rossi makes it even more appealing. Adding more dependencies to a project can cause unwanted side-effects, when one of these dependencies e.g. has security issues1,2.
The core idea behind errore can be broken down to just 6 LOC
// You can write this without installing errore at allclass NotFoundError extends Error { readonly _tag = 'NotFoundError' constructor(public id: string) { super(`User ${id} not found`) }}
async function getUser(id: string): Promise<User | NotFoundError> { const user = await db.find(id) if (!user) return new NotFoundError(id) return user}
const user = await getUser('123')if (user instanceof Error) return userconsole.log(user.name)