Programs are full of errors. This is expected 💁🏼♂️
As such, languages offer tools to deal with errors, aka error handling.
The community is starting to see how errors are a key part of software development, not an afterthought.
Let's learn some patterns to deal with errors 👇
An issue of throwing and catching
Most languages went down the path of throw
/catch
.
The concept sounds good and simple (in theory):
- When something goes wrong use
throw
, it will stop everything and crash - When you want to handle (any) error use
catch
to prevent crashing and do something else
It gets as concise as it can be:
const mightError = () => {
if (Math.random() > 0.5) {
throw new Error();
}
return 'success';
};
/// 👇 Somewhere else up the chain
const main = () => {
try {
// Do something, regardless of possible errors
} catch (e) {
// Handle (any) error
}
};
It gets bad, fast
Now, what's the problem with this?
2 core issues:
- Handling multiple errors becomes a nightmare
- The error type inside catch is
unknown
If we want to handle errors in different ways we get 2 options:
throw
custom error instances, and useinstanceof
insidecatch
class MyError extends Error {}
const mightError = () => {
if (Math.random() > 0.5) {
throw new MyError();
}
return 'success';
};
/// 👇 Somewhere else up the chain
const main = () => {
try {
// Do something, regardless of possible errors
} catch (e) {
if (e instanceof MyError) {
// throw of custom error (somewhere 💁🏼♂️)
}
throw e; // 👈 throw again other errors
}
};
- Use multiple
try
/catch
const mightError1 = () => //
const mightError2 = () => //
/// 👇 Somewhere else up the chain
const main = () => {
try {
mightError1();
} catch (e) {
// Handle errors (part 1)
}
try {
mightError2();
} catch (e) {
// Handle errors (part 2)
}
// And more if needed 😐
};
No surprise that devs avoid this all together. It's not fun, not practical, and it gets complex, fast.
This bad impression justifies the push-back of some devs on the topic of error handling: "Better not to deal with this" 😶
Better not to throw
Some people realized that throw
is "bad", and that maybe we can treat errors as values.
Simple solution is returning 2 values: error
or data
.
const mightError = () => {
if (Math.random() > 0.5) {
return { error: new Error() };
}
return { data: 'success' };
};
const main = () => {
const { data, error } = mightError();
if (error !== undefined) {
// We got an error, do something!
}
};
You get many variations of this pattern, using arrays, objects, union types; anything that can contain "2 things":
Here's my favourite method for getting Go/Zig/Rust-like errors in TS. It's by returning custom errors from functions. - Shows right in the return type - Easy checking with instanceof - No deps required
It gets ugly, fast
Some improvements compared to throw
, but the experience is still "cranky".
if
becomes an integral part of the error handling strategy, and it looks bad:
const mightError1 = () => //
const mightError2 = () => //
const main = () => {
const { data: data1, error: error1 } = mightError1();
if (error1 !== undefined) {
// We got an error, do something!
}
const { data: data2, error: error2 } = mightError2();
if (error2 !== undefined) {
// We got an error, do something!
}
// And more if needed 😐
};
Each function that "might error" needs a new if
to check for errors and stop the program flow. Not good.
There is even a proposal to add a new
?=
syntax to make this even more common 👇const [error, response] ?= await fetch("https://arthur.place");
Not good if you ask me 👀
Result type someone?
There is another solution. It comes from the obscure world of functional programming, it's called Either (Result) type.
It's something that can have either an error or a success value:
class Left<L, R> {
readonly _tag: 'Left' = 'Left';
constructor(readonly left: L) {}
}
class Right<L, R> {
readonly _tag: 'Right' = 'Right';
constructor(readonly right: R) {}
}
export type Either<R, L> = Left<L, R> | Right<L, R>;
const mightError = (): Either<string, Error> => {
if (Math.random() > 0.5) {
return new Left(new Error());
}
return new Right('success');
};
There's only one concept you need to know in neverthrow: `Result`. A `Result` represents the result of a computation that might fail. It can either It's either `Ok` with a value, or `Err` with an error.
Nice nice. Not enough.
This has the same issue as before, we need to check for errors at each step:
const main = () => {
const value1 = mightError1();
if (value1._tag === "Left") {
// We got an error, do something!
}
const value2 = mightError2();
if (value2._tag === "Left") {
// We got an error, do something!
}
// And more if needed 😐
};
Welcome to monads (that's right)
We need something that:
- Collects errors as values
- Let's us move on as if errors are not there
- Allows to handle all the collected errors (all together or one by one)
Solution (again) from functional programming. It's (technically) called monad.
In practice you see it in the wild as flatMap
/andThen
/bind
.
That's how it looks like in Effect 👇
Same code using Effect:
I'm loving neverthrow. Type-safe error handling in TypeScript without the bullshit. Let me show you how it works 🧵
Generators (yield*
) collect possible errors at each step, while at the same time returning the success value for us to use.
Only at the end you can extract the return value similar to Either
(error or success).
For a full breakdown of the core issue with error handling in typescript check out this video 👈
My effect course was published last week. Definitely a positive response 💯
I've just finished reading it. 🚀 - Super intuitive example (API fetch) - Well-structured: The organization of topics made it easy to follow along. - I like the "let's refactor this" parts - You won't get far without coding while you read: This hands-on approach is crucial for…
@EffectTS_ Beginners Complete Getting Started course is out now 🚀 From zero to building complete apps with effect Type-safe, maintainable, testable apps, and it's just typescript 💯 Here is what you'll learn 👇🧵 typeonce.dev/course/effect-…
Meanwhile I am working on the next in the pipeline: XState course.
Stay tuned, I am coming strong into Q4 this end of 2024 🔜
See you next 👋