Why Every Programming Language Sucks at Error Handling
blog.aiono.dev
blog.aiono.dev
I've been using https://github.com/biw/enwrap (disclaimer: I wrote it) in TypeScript and have found that the overhead it adds is well worth the safety it adds when handling and returning errors to users.
That said, I see parallels between the debate about typed vs. non-typed errors and the debate of static typing vs. dynamic typing in programming languages.
Author of the post here. I also see this parallel in error handling discussions. But seems like it's much harder to sell error handling than static typing. Static typing was also much more debatable in the past then now so maybe same can happen to error handling mechanisms in the future as well.
Your project seems very interesting! Typescript is sophisticated enough to model complex Result-like types that can narrow-widen error cases throughout the code. I will check it more if I find the time.
With errors, it's hard since many languages can throw errors anywhere, so it's hard to feel like any function is "safe" in terms of error handling. That's one of the reasons why `enwrap` returns a generic error alongside any other result: to support incremental adoption.
If you have a chance to check out `enwrap` and have feedback, email me! (link in bio)
In a lot of the non-trivial systems I've worked on, the setup to do debugging/development work to get to a particular part of the code can often be arduous and time consuming, meaning that if you made a mistake and have to restart your debugging session you then have to go through the setup process again which radically slows down progress on thorny issues.
In Smalltalk it was possible to take an unhandled exception, re-inject a more suitable value and continue debugging so that each "session" was far more productive and the setup issue became much less onerous.
Sadly, .NET, Java, Javascript and Python can't even come close to the power of that ability to catch an exception, modify the object and continue execution. It's a great loss in my opinion. So much of the Smalltalk programming environment has been copied into modern languages but not re-entrant exceptions.
Now, while it was very powerful it could also be heavily abused including at run time, but I still miss that ability.
Is that...On Error Resume Next? :D
It's not just about being able to stop your program and hot-patch code to recover from errors - this should be trivial in any dynamic language. Rather, it's about being able to composably control how to recover from exception conditions (or, really, any branching behavior) both interactively and programmatically in a unified framework.
Although the same statement is used to catch both, you only catch both if you catch Exception. If you catch IOException or whatever other exceptions you need to catch and you opt to handle them, then RuntimeException will still propagate.
It is only a matter of understanding that this is the distinction, and writing the bodies of your catch blocks accordingly.
And if you are only writing a prototype, declare that it throws Exception, or if you can't, a catch (IOException ex) { throw new UncheckedIOException(ex); } really isn't that bad.
> And if you are only writing a prototype, declare that it throws Exception, or if you can't, a catch (IOException ex) { throw new UncheckedIOException(ex); } really isn't that bad.
The problem with that is when you go back to do proper error handling you can't easily find all those places where you did this. For instance in Rust, you can find those by searching for `unwrap`.
Philosophically, I don't differentiate how you represent errors, they are all errors in the end. You can use unchecked exceptions, results, checked exceptions, error codes, whatever. I will abide by rules set in the code base's guidelines, and I do have my preferences (unchecked exceptions), and I'll also accept using one or the other for practical reasons (performance, or binary overhead), but I will never argue that it is absolutely best that one error handling scheme is reserved for uses ABC, and the other best for EFG.
A bug is one of two things, writing code that is erroneous despite having knowledge at compile time that it can't happen such as, indexing into an array of known bounds with inputs that are known to never be out of bounds. Typically where asserts are used. You can promote these types of bugs to errors if it doesn't harm your system stability, and they often are in order to avoid crashing. The other definition of a bug is just your program entering in an unforseen state.
I dislike using the words expected, unexpected, and such for error handling unless you're talking about c++'s std::expected, because no one can agree what they mean, hence to me are useless definitions when talking about error handling. While the same can be said about definitions of errors and bugs, the practical uses tend to converge to an agreement on whether something is an error, or a bug.
From years of observing error handling:
- Most times I see people talk about it, from what I interpret from what people say, seems to have an emphasis on what operations can error out, rather than emphasizing what the state of your program should be when a unit of code errors out, which I believe should be more emphasized.
I typically define a unit of code as a function because that's how most things already work and is easier to reason about. In other words, stop focusing on individual failures of the callers, rather treat the entirety of the callee as a failure. This reduces your function to usually only have 2 states, what should your program state be when everything succeeds, and what should it be when it fails.
That's how you can interpret a lot of value based error handling functions already no matter how many operations can fail. With exceptions it means that, either your whole function must be in a try/catch block, or it doesn't have one at all.
- Value based error handling seems to re-invent some forms of unchecked exception features, with very good reasons, leading me to believe that they are on the same sides of the coin where given enough language and tooling support, they converge to the exact same thing.
- I do think it's better to encode preconditions in some way visible to callers that's not just through documentation. If you're using types, which is my preferred way, known as dependent types. I'm not too familiar with them, but apparently requires a proof solver for it to work entirely at compile time. I wouldn't mind them turning into runtime checks, and if you have overloading enabled, allows runtime checks to mostly occur once, at construction time.