I am thinking about this topic for 4 months+ now. I know different languages and different approaches the 3 mentioned are just the most obvious ones and can be combined.
To me the question about error handling is about information, to the maintaining person you want to have meaningful information "how/why/where/what" they have to be granular.
For the GUI you need meaningful information but only the essential information "x y z could be done or not ... please do ..."
So the thing is, The exception model has its merits you can easily build a strict contractual model for the Errors
methodX(always-in): always-out
This is cool for side-effects like database access or file reads and you can remove a lot of "if else" cases that basically bloat your code by just placing them in a catch statement.
But than you still need to pass meaningful information to the GUI which can be an error code or you catch the "systemException" and "re-throw a Meaningful" one.
But as soon as you go for batch processing this model suddenly can become a lot of overhead. An ErrorObject (like go) approach can be much more meaningful and less resource costly since having 30 times the same error with just a different parameter is TMI as well as useless computation. But sadly not every language support two return parameters. Hence in a classic OOP architecture you will have a error Queue or you use the signature-contract-precision (in a dynamic language).
So anyhow .... I did think about 5 different core approaches and their trade-offs and their combinations.
Maybe someone could add something meaningful to my process, I would appreciate it.
Important to me is the reasoning! Why you chose that way and how you handle the gap between "essential for the user" vs "essential for the maintainer".
I've also thought about this, and I've changed preference from checked + unchecked exceptions (like Java) to a built-in ok|err enum plus panics (like Rust).
There are several reasons for preferring returning return/err type:
I'm a little sad to even mention the first two points, but there is still a lot of code that uses true/false to indicate success/failure and makes the caller get the information about reason etc.
Note that I'm focusing on the return value, but once it becomes apparent that a problem isn't recoverable, panic (or runtime exception) is a good solution and some of the above considerations no longer apply.
I feel that irrecoverable situations are either 1) out of control, so no need to design around it, or 2) things that are bugs if they happen, so write fixes instead of error handling.
EDIT: note that this is possible, but not very convenient, in many languages. The language should have sum types (not crippled enums that only work with primitives), a shortcut for propagating error return values, and a way to warn/error if a return value is not used.
Depends on what's causing the error and the language, though as a rule I would often say "all of the above" -- such as in PHP having a singleton object that handles both regular errors and exceptions outputting the correct error codes both client-side (whilst avoiding revealing any details that could be used by crackers) and into a permanent log.
Or in JavaScript where I'll make an object that has my error test conditions as methods which then either console.logs, console.warn, or does a Throw new Error as appropriate to the error in question... whilst also exposing those (with legacy wrappers to a object at the end of document.body for pre-console browsers if needed) log/warn/throw methods so that they can be called by future code if need be whilst maintaining a consistent error behavior.
I actually kind-of worry when people advocate something like a specific error handling method to the exclusion of all others. It begs the question "then why to do the others exist?" -- Like the clowns right now screaming "never use exceptions in PHP" -- RIGHT... tell me another one Josephine.
I've even had some jokers say you should never use "Throw" in JavaScript because it halts scripting execution... well duh, THAT'S WHY I'M USING IT!!!
Excellent recap.
I love returning error objects in Go, because of the easy to read and "lightweightness" (is that a word?) of the code: easy to read, easy to understand, easy to maintain, but without sacrificing the important pieces. And whenever you call a method which might return an error.
Error code ar estill very useful though, especially in logs. (I like to return error objects, which contains code so that I can get consistent logging)
I think that Rust's approach is very good and superior to everything else I know. It's even something which can be implemented into other languages.
Basically, there is no throw or additional channel, which communicates errors. Instead, when something can go wrong, you simply return a
Result<T, E>object, which has to be handled in order to process the result. What you can now do is either handle the error and try another call, or have the error bubble up through all function calls, until it hits an API or user-facing code, which then can automatically handle the exception, or display it. The advantage the regular function result is used to communicate the whole result of a call (not only the success one), and it enforces proper error handling without requiring developers to read the docs to find out that your function actually might throw an exception.In Rust, there are additional libs, which add error-chaining, scoped error handling, etc. to the mix, but all that stuff build on top of the core mechanic of Results, which are used throughout the std lib :)
Another piece of the puzzle to better error management is to get rid of error whenever possible. For example by using optional values. In all cases, where there might be a value, or might not be a value, you just say, that there is an object, which stores the state of something being there or not, and if there is something, also stores the value. That way devs are forced to handle the case when there is nothing. Not doing so is more difficult than just hoping that there is no
null. Again, Rust has a stdlib struct for that:Option<T>.