At page 46, the author asserts that using exceptions is better than returning error codes from your functions.
I heavily disagree, as this will separate logic in at least 2 pieces, with the error being handled elsewhere. I do find it excusable for certain contexts, like I/O (ie have a function that reads from a file raise an exception, but that exception should not be bumped up the call stack more than one step).
The design principle is also weakened by the possibility of returning multiple values, or to return a struct/class that contains both data and error.
I wonder what is the consensus on this design issue is, more than 10 years later.
> I heavily disagree, as this will separate logic in at least 2 pieces, with the error being handled elsewhere.
1. Triggering and handling an error are two separate things. They can be on opposite sides of a library call, a process boundary, or a network boundary. Depending on the nature of the error, the logic that triggers it may be completely oblivious about how zo handle it.
2. Even if ignoring #1, returning an error code might propagate through the call stack, too, if call sites choose to do so.
My thoughts:
There is no one true best thing. IMHO the most important thing is consistency. If you provide a Java API, then go ahead and throw exceptions, as is customary. In Go, return an idiomatic (result, err) tuple. If you're writing Typescript, consider using a union result type, maybe one that has a discriminating status field, if your failure mode is common enough that you prefer to force your callers to handle it. And if you're writing a C library, then go ahead and return EBADWIDGET or whatnot. Just keep it consistent to the rest of the code base and to developers's expectations.
If your language has both result types and exception types, and either is idiomatic, then ask yourself how strictly you want to force your clients to handle errors, and make your choice with that in mind.
Exceptions in modern languages are essentially that with a more hierarchal structure, because they will auto propagate up the chain, into the exception clause that can go to cleanup code (or in the finally clause)
This is where exception handling is best used for, which requires a minimal number of try statements in critical pieces of the program (if you are handling exceptions from functions and continuing as normal, you are better off using returned error codes)
Never return error codes, always return results. A function that reads a file into lines and you can't do that? Throw exception. Can't restart some service? Again, throw. Throwing is almost always right, unless the failure itself is a legitimate result. For example, a parser should probably not throw on errors and should instead return a ParseResult which is either a ParseTree or a ParseError containing (at least) an error code for the computer and an error message for the human. However, a parser should never return an error for say, a divide by zero or out of memory error (though a garbage collector could), since those aren't really parse errors.
See https://wiki.c2.com/?SamuraiPrinciple and related pages.
Even if you just pass the error up the call chain, it's good to have that explicitly expressed in the code. For critical software Error paths should be fist class citizens of the logic, not some "magic" path that may not be expressed in the source at all.
Exceptions are sometimes good for things going totally wrong, like connection failures or disk full. What I don't like is that it is a pretty big (and sometimes slow) concept that is often misused as 'message bus'.
However, error codes are quite limited and often are an extra load besides the necessary return value, requiring reference or out params.
The best concept i know of is rust style error handling, wrapping returns via generics providing a success and an error type. This is very specific, doesn't lose context, pretty quick and still elegant to handle. There are also libraries using this style in other languages, e.g. C#[1], but it does not work everywhere.