Most advice concerning error handling boils down to a handful of tips and tricks (see this post for example). These hints are helpful but I think they don\'t answer all question
Disclaimer: I do not know any theory on error-handling, I did, however, thought repetitively about the subject as I explored various languages and programming paradigms, as well as toyed around with programming language designs (and discussed them). What follows, thus, is a summary of my experience so far; with objective arguments.
Note: this should cover all the questions, but I did not even try to address them in order, preferring instead a structured presentation. At the end of each section, I present a succinct answer to those questions it answered, for clarity.
Introduction
As a premise, I would like to note that whatever is subject to discussion some parameters must be kept in mind when designing a library (or reusable code).
The author cannot hope to fathom how this library will be used, and should thus avoid strategies that make integration more difficult than it should. The most glaring defect would be relying on globally shared state; thread-local shared state can also be a nightmare for interactions with coroutines/green-threads. The use of such coroutines and threads also highlight that synchronization best be left to the user, in single-threaded code it will mean none (best performance), whilst in coroutines and green-threads the user is best suited to implement (or use existing implementations of) dedicated synchronization mechanisms.
That being said, when library are for internal use only, global or thread-local variables might be convenient; if used, they should be clearly documented as a technical limitation.
Logging
There are many ways to log messages:
As the author of a library, the logs should be integrated within the client infrastructure (or turned off). This is best provided by allowing the client to provide hooks so as to deal with the logs themselves, my recommendation is:
stdout
and stderr
(depending on severity), until the client explicitly says not to logI would note that, following the guidelines delineated in the introduction, synchronization is left to the client.
Regarding whether to log errors: do not log (as errors) what you otherwise already report via your API; you can however still log at a lower severity the details. The client can decide whether to report or not when handling the error, and for example choose not to report it if this was just a speculative call.
Note: some information should not make it into the logs and some other pieces are best obfuscated. For example, passwords should not be logged, and Credit-Card or Passport/Social Security Numbers are best obfuscated (partly at least). In a library designed for such sensitive information, this can be done during logging; otherwise the application should take care of this.
Is logging something that should only be done in application code? Or is it ok to do some logging from library code.
Application code should decide the policy. Whether a library logs or not depends on whether it needs to.
Going on after an error ?
Before we actually talk about reporting errors, the first question we should ask is whether the error should be reported (for handling) or if things are so wrong that aborting the current process is clearly the best policy.
This is certainly a tricky topic. In general, I would advise to design such that going on is an option, with a purge/reset if necessary. If this cannot be achieved in certain cases, then those cases should provoke an abortion of the process.
Note: on some systems, it is possible to get a memory-dump of the process. If an application handles sensitive data (password, credit-cards, passports, ...), it is best deactivated in production (but can be used during development).
Note: it can be interesting to have a debug switch that transforms a portion of the error-reporting calls into abortions with a memory-dump to assist debugging during development.
Reporting an error
The occurrence of an error signifies that the contract of a function/interface could not be fulfilled. This has several consequences:
The latter point will be treated later on; for now let us focus on reporting the error. The client should not, ever, be able to accidentally ignore this report. Which is why using error-codes is such an abomination (in languages when return values can be ignored):
ErrorStatus_t doit(Input const* input, Output* output);
I know of two schemes that require explicit action on the client part:
optional
, either
, ...)The former is well-known, the latter is very much used in functional languages and was introduced in C++11 under the guise of std::future
though other implementations exist.
I advise to prefer the latter, when possible, as it easier to fathom, but revert to exceptions when no result is expected. Contrast:
Option find(Key const&);
void updateName(Client::Id id, Client::Name name);
In the case of "write-only" operations such as updateName
, the client has no use for a result. It could be introduced, but it would be easy to forget the check.
Reverting to exceptions also occur when a result type is impractical, or insufficient to convey the details:
Option compute(RepositoryInterface&, Details...);
In such a case of externally defined callback, there is an almost infinite list of potential failures. The implementation could use the network, a database, the filesystem, ... in this case, and in order to report errors accurately:
The goal is to let this exception bubble up to the layer where the implementation of the interface was decided (at least), for it's only at this level that there is a chance to correctly interpret the exception thrown.
Note: the externally defined callback is not forced to use exceptions, we should just expect it might be using some.
Using an error
In order to use an error report, the client need enough information to take a decision. Structured information, such as error codes or exception types, should be preferred (for automatic actions) and additional information (message, stack, ...) can be provided in a non-structured way (for humans to investigate).
It would be best if a function clearly documented all possible failure modes: when they occur and how they are reported. However, especially in case arbitrary code is executed, the client should be prepared to deal with unknown codes/exceptions.
A notable exception is, of course, result types: boost::variant
provides a compiler-checked exhaustive list of known failure modes... though a function returning this type could still throw, of course.
How to decide between logging an error, or showing it as an error message to the user?
The user should always be warned when its order could not be fulfilled, however a user-friendly (understandable) message should be displayed. If possible, advices or work-arounds should be presented as well. Details are for investigating teams.
Recovering from an error ?
Last, but certainly not least, comes the truly frightening part about errors: recovery.
This is something that databases (real ones) are so good for: transaction-like semantics. If anything unexpected occurs, the transaction is aborted as if nothing had happened.
In the real world, things are not simple. The simple example of cancelling an e-mail sent pops to mind: too late. Protocols may exist, depending on your application domain, but this is out of this discussion. The first step, though, is the ability to recover a sane in-memory state; and that is far from being simple in most languages (and STM can only do so much today).
First of all, an illustration of the challenge:
void update(Client& client, Client::Name name, Client::Address address) {
client.update(std::move(name));
client.update(std::move(address)); // Throws
}
Now, after updating the address failed, I am left with a half-updated client
. What can I do ?
In any case, the book-keeping required is such that mistakes will creep in.
And worst of all: there is no safe assumption that can be made as to the extent of the corruption (except that client
is now botched). Or at least, no assumption that will endure time (and code changes).
As often, the only way to win is not to play.
A possible solution: Transactions
Wherever possible, the key idea is to define macro functions, that will either fail or produce the expected result. Those are our transactions. And their form is invariant:
Either
A transaction does not modify any external state, thus if it fails to produce a result:
Any function that is not a transaction should be considered as having corrupted anything it touched, and thus the only sane way of dealing with an error from non-transactional functions is to let it bubble up until a transaction layer is reached. Any attempt to deal with the error prior is, in the end, doomed to fail.
How to decide if an error should be handled locally or propagated to higher level code ?
In case of exceptions, where should you generally catch them? In low-level or higher level code?
Deal with them whenever it is safe to do so and there is value in doing so. Most notably, it's okay to catch an error, check if it can be dealt with locally, and then either deal with it or pass it up.
Should you strive for a unified error handling strategy through all layers of code, or try to develop a system that can adapt itself to a variety of error handling strategies (in order to be able to deal with errors from 3rd party libraries).
I did not address this question previously, however I believe it is clear than the approach I highlighted is already dual since it consists of both result-types and exceptions. As such, dealing with 3rd party libraries should be a cinch, though I do advise wrapping them anyway for other reasons (3rd party code is better insulated beyond a business-oriented interface tasked with the impedance adaption).