Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@CMCDragonkai
Last active January 8, 2024 16:45
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save CMCDragonkai/3668455056efd30d0dc93d3f45fb6150 to your computer and use it in GitHub Desktop.
Save CMCDragonkai/3668455056efd30d0dc93d3f45fb6150 to your computer and use it in GitHub Desktop.
Error and Exception Handling Techniques

Error and Exception Handling Techniques

http://lambda-the-ultimate.org/node/3896#comment-58374

Reposting here for safe keeping.

Error Codes

One of the return values is the error. Use when the goal is to cover-your-ass by ensuring that error handling is possible, even though you know any error handling or recovery would clutter the happy-path and thus ensure programmers are reluctant to admit to their existence at all.

Product Types (i.e. (Result,Error) pairs)

Error codes, reformulated for language with efficient tuples. Same issues apply.

Ambient Error Value (Errno)

Error information is stored in some ambient space, such as a global variable, thread-local storage, or dynamic scope. This allows you to simplify the interface for the happy-path, and thus make it even easier than error codes for developers using your API to completely ignore error handling.

Null Object or Error Object

For OO. Constructors for common classes of 'error' are recognized in a superclass. I.e. "IDivByZero" might be a subclass of "Integer" returned after a division by zero. These objects report reasonably sane values and have sane behaviors for most queries and operations, to avoid crashes. The error can be recognized by extra interfaces of Integer that indicate whether the object is truly an error or not (and perhaps provide an enumeration for which class of error). Use this Error Object pattern if your goal is to ensure that an error in one part of the program will lurk around like a submarine, waiting for the opportunity to sink some other part of the program before vanishing, without a trace, into the bloody waters.

Sum Types (i.e. Maybe monad)

Return value distinguishes between success, and handling the different cases is enforced at the type level. Use when the goal is to force programmers to at least recognize the error, if not recover from it. A little syntactic sugar for a hidden variable can go a long way towards avoiding clutter.

Error Logging

Keep a trace of activities and errors so that, when when everything goes to hell, you at least have a record of the good intentions that paved the road.

Pass a Handler (Error Counseling)

Error-handlers are represented explicitly and passed into the program (via argument, or dependency injection, or dynamic scope, or thread-local storage). When the error occurs, call the appropriate error-handler with the necessary arguments. The error-handler either represents a continuation (in which case you call it from a tail-recursive position), or will inform you, i.e. via return value, of how you are to continue after the error. This is a flexible and powerful mechanism, and works well in combination with almost any other error handling mechanism (except for resumable exceptions, which effectively subsume this pattern).

Exceptions

Include a non-local exit so that error-handling may be isolated upon the stack. Unfortunately, in many languages this "unwinds" the "stack", and thus a lot of important, unfinished work is simply lost, and the error-handling policy cannot effectively say anything useful about error recovery. Beware, also, that exceptions have a dire impact on parallelization and partial-evaluation (beta reduction), especially in the face of modularity. The main reason: unless you are also okay with going non-deterministic, exceptions force you to give exceptions a 'precedence' based on an evaluation order. Sum-type and Pass-a-handler does not have this problem. (OTOH, if you need a well-defined evaluation order regardless, then exceptions won't hurt you in this regard.)

Resumable Exceptions

The unfinished work is maintained during error handling, and there are mechanisms available to return to that work - i.e. by return value, or via 'resumption points' annotated in the code (which look the same as exception handlers). This allows a great deal of flexibility for what those error-handling policy can express, similar to the pass-a-handler approach. The greater structure, however, can lead to better performance and static analysis. Usefully, the difference between resumable exceptions and regular exceptions only requires a very minor tweak in implementation, even in languages such as C++ and Java: handle the exception as a new activation at the top of the stack, rather than unwinding first. (These activation records would need to include a pointer to the original frame in addition to the stack pointer.) This is exactly what Dylan does.

Deferred Exception Object

Similar to Error Object pattern, except that the "sane responses to most queries" are replaced by "throws an exception for most operations", thus turning this object into a neat little grenade. Only a few interfaces will not throw an exception, such as checking whether the object represents an error, and which error it represents. Thus, you have opportunity to recognize and dispose of it properly rather than pass it into some delicate part of the computational machinery that doesn't expect to suffer concussion damage.

Transactions

The error didn't happen. (Handwave.) Nothing else happened either, making this a rather inflexible failure mode. Very powerful. Equivalently, very difficult to make this work at the interfaces between systems, i.e. with sensors and effectors, though an ad-hoc approach may be sufficient and XOpen/XA might help a bit.

Do or Die (Let it Crash)

If you can't complete, engage in a manic bout of parricide followed by suicide. The primary advantage of this approach is that dying early, and dying fast, is dead-obvious to the developers, and thus forces them to handle the problem rather than shipping it. (One hopes.)

Resilience and Self Healing

Build death and rebirth into the language or architecture. Erlang is well known for this style of error handling; it uses 'let it crash' along with supervisors for recovery. You need high-level resilience and cascading destruction because the high-level is where you have the most knowledge about how to recover from certain partial-failures without breaking your clients. Low-level resilience is useful for performance, but only when you can ensure that the recovery doesn't violate any non-local semantics.

I'm fond of sum-type error handling, pass-a-handler, and effective resilience (by minimizing need for state). I've gone through stages where, for a few years, I was enamored with resumable exceptions and later with transactions. But simplicity, modularity, and local reasoning are not well served by exception handling and transaction mechanisms.

-- dmbarbour

From also reading https://github.com/fpco/safe-exceptions and https://www.schoolofhaskell.com/school/starting-with-haskell/basics-of-haskell/10_Error_Handling my view on errors and exceptions is that first we should separate between the 2.

Errors are things that are naturally part of the current control flow abstraction. For example parsing or validation or business logic. Errors are expected to occur, and form part of a backtracking or transactional framework. From this idea, you can use all sorts of error handling techniques listed above to deal with this, but if you can rely on something that is constructive within the language rather than something that is a builtin runtime primitive, then the resulting code should be more generic and composable (even across language runtimes). Error handling should never result in a runtime crash.

Exceptions are truly exceptional things relative to the program that cannot be handled at the current abstraction level. For example in Haskell, exceptions can be thrown anywhere (even in pure functions) but can only be handled in the IO monad context. This implies that exceptions are meant for errors deriving from interactions with the wild outside world of IO, hence they eventually propagate to a catch-all handler at the main :: IO () context. Even if you don't supply your own catch-all handler, there is always one available and all it does is propagate it to the parent process and perhaps even the end-user. This philosophy I think is also what drives the community of developers that argue that exceptions shouldn't be used for "control flow". I think what these developers are referring to is error handling in the previous paragraph.

The concept of an error can subsume the concept of an exception at different levels of abstraction. For example an exception relative to a userspace process can be considered an error at the operating system context. An exception at the operating system context can be considered an error at the distributed cluster context.

Given these 2 categories, we can then begin to discuss and consider all the different variations of errors and exception handling techniques and compare their trade offs.

We have to be careful here, as many languages have a native builtin concept of an exception which does not meet the above categories. They may allow exceptions to be thrown anywhere and caught anywhere. This muddies the conceptual clarity of exception and error handling, and we should be aware when we are talking about a particular language's implementation of an "exception", and the above general idea of an exception compared to an error. Also other authors may swap the definitions around, instead calling exceptions errors, and errors exceptions.

@CMCDragonkai
Copy link
Author

This is a great exploration of errors/exceptions here: http://joeduffyblog.com/2016/02/07/the-error-model/

@CMCDragonkai
Copy link
Author

One thing I've discovered that at least on Linux operating systems, errors across process boundaries get collapsed into a 1 dimensional construct. It's basically just an integer with 0 as the sentinel value for success. The stdout and stderr streams are more designed for interactive interpretation, not machine recovery. That is we generally don't automatically process the stderr and figure out how to recover from that error because the stderr stream is usually designed to be human readable, not machine parseable. So basically a C-like error code. It would be really interesting if there was richer exceptions as an IPC mechanism.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment