🪲 Errors should be returned, not thrown
In short, I used to approach this topic delicately, like "it's useful", "it would be cool if you did it this way", "well, if you decide to" and so on, but now I'm completely convinced that this is the right approach and I don't want to suggest anything else.
This approach has a lot of advantages: (1) you can clearly see what types of errors are being returned (especially if you create custom errors), (2) there are no implicit error catchers, (3) try is a cumbersome and ugly construct that can add several levels of nesting and many other useful points.
And there was always a conceptual question, which is the subject of many conferences and articles: why should I throw an error that is a normal part of business logic (for example, NotFound on an entity that may not exist) when it can kill my program if it's not caught.
What convinced me so strongly?
- Historically, it was this way in C.
- Then Go adopted this feature.
- In Rust, it's the same story, BUT they return a Result type monad <T, Error>.
- And then I found out that in Zig (the language I'm currently betting on) there's a built-in Union Type operator for returning errors.
And I won't even mention functional languages, where this is just the standard, also through monads.
If you use TS, the most adequate option is: type Result<T, E extends Error = Error> = T | E – I can explain why later.
Yes, there are some places (for example, validating individual properties) where this can make the code messy, but there's a solution for that too (I'll talk about it in the next posts)
Now I'm curious: how much do you like the idea of returning errors instead of throwing them?