There are various approaches and philosophies regarding error handling in different programming languages. This article tries to give an overview.
Most of the current mainstream programming languages use exceptions for error handling. When an exception is raised (“thrown”), the call stack is being unwound until the exception is caught. The functions passed along the way through the call stack have to ensure that any opened resources are properly closed. This is usually done via finally blocks. If the exception is not caught the program terminates.
Exceptions come in two flavors: checked exceptions and unchecked exceptions. The handling of checked exceptions is enforced by the compiler. Checked exceptions are part of the function signatures. A function explicitly declares in its signature what exceptions can be thrown:
void f() throws A, B, C
The caller of a function has to either handle the exceptions (fully or partially) or let them pass through by re-declaring them in the throws clause of its own function signature.
Checked exceptions have the property that it’s hard to forget to handle them. However, proponents of unchecked exceptions argue that checked exceptions have two problems: versioning and scalability.
Once declared they are part of the interface and adding another exception will break all client code. Multiple exception types also tend to accumulate the more different subsystems are being aggregated. Proponents of unchecked exceptions prefer a catch-all clause further up the call stack. Some languages (e.g. Erlang) even follow a “let it crash” paradigm and simply respawn crashed processes. This approach is more viable in distributed systems than in user-facing applications.
Java is known for its checked exceptions. C#, C++, Scala and most dynamically typed languages decided to go with unchecked exceptions.
An alternative to exceptions is no exceptions. Exceptions overlay multiple different control flows, which makes it harder to reason about the control flow of a function. With exceptions functions can return at many other points than the explicit return points.
If an error is just a value that is returned by a function it can be handled by the usual control flow mechanisms of a language (like if and else) without the need of a special sub-language for error handling. These errors tend to be handled closer to the place of their occurrence rather than further up the call stack.
In such a language, which uses return values to flag errors, you’d better check all errors, otherwise you risk continuing with an incorrect, invalid or meaningless value. This can be enforced either by the compiler or via a lint tool.
There are different possibilities of how an error could be returned from a function:
In C a sentinel value in the range of the return type is often used to indicate an error, e.g. a negative value or zero. This is not a good solution, because it intermingles two things that do not belong together and it limits the range of valid return values. Another solution in C could be the use of an error output parameter. Prominent examples are NSError in Objective C or GError in GLib. This brings us to another possibility:
Some languages support multiple return values (e.g. Go) or tuple types (product types), which can act as multiple return values. One value can hold the actual result (e.g. number of bytes written), the other can indicate an error.
Multiple return types / product types are a simple solution, cover the necessary use cases and require little additional language support. A more sophisiticated and more restrictive solution are sum types, but they require a little bit more language support: instead of returning a value AND a possible error, a function returns either a value OR an error. This way the programmer is forced to check for an error by discriminating between the two cases. This is usually done via a feature called structural pattern matching (not to be confused with pattern matching on strings), either explicitly with a switch/case-like control structure or implicitly via convenience function. A popular example is Haskell’s Maybe monad or the Option or similarly named type in some other languages (e.g. Scala, Standard ML, OCaml).