Error Handling Strategies
Learn about the four main error handling strategies- try/catch, explicit returns, either, and supervising crashes- and how they work in various languages.
Join the DZone community and get the full member experience.
Join For FreeIntroduction
There are various ways of handling errors in programming. This includes not handling it. Many languages have created more modern ways of error handling. An error is when the program, intentionally, but mostly not, breaks. Below, I'll cover the 4 main ones I know: try/catch, explicit returns, either, and supervising crashes. We'll compare various languages on how they approach errors: Python, JavaScript, Lua, Go, Scala, Akka, and Elixir. Once you understand how the newer ways work, hopefully, this will encourage you to abandon using potentially program crashing errors via the dated throw
/ raise
in your programs.
Contents
When programs crash, log/print/trace statements and errors aren't always helpful to quickly know what went wrong. Logs, if there are at all, tell a story you have to decipher. Errors, if there, sometimes give you a long stack trace, which often isn't long enough. Asynchronous code can make this harder. Worse, both are often completely unrelated to the root cause, or lie and point you in the completely wrong debugging direction. Overall, most crashes aren't always helpful in debugging why your code broke.
Various error handling strategies can prevent these problems.
The original way to implement an error handling strategy is to throw your own errors.
// a type example
validNumber = n => _.isNumber(n) && _.isNaN(n) === false;
add = (a, b) => {
if (validNumber(a) === false) {
throw new Error(`a is an invalid number, you sent: ${a}`);
}
if (validNumber(b) === false) {
throw new Error(`b is an invalid number, you sent: ${b}`);
}
return a + b;
};
add('cow', new Date()); // throws
They have helpful and negative effects that take pages of text to explain. The reason you shouldn't use them is they can crash your program. While often this is often intentional by the developer, you could negatively affect things outside your code base like a user's data, logs, and this often trickles down to user experience. It also makes it more difficult for the developer debugging to pinpoint exactly where it failed and why.
I jokingly call them explosions because they accidentally can affect completely unrelated parts of the code when they go off. Compilers and runtime errors in sync and async code still haven't gotten good enough (except maybe Elm) to help us immediately diagnose what we, or someone else, did wrong.
We don't want to crash a piece of code or entire programs.
We want to correctly identify what went wrong, give enough information for the developer to debug and/or react, and ensure that error is testable. Intentional developer throws attempt to tell you what went wrong and where, but they don't play nice together, often mask the real issue in cascading failures, and while sometimes testable in isolation, they are harder to test in larger composed programs.
The second option is what Go, Lua, and sometimes Elixir do, where you handle the possible error on a per function basis. They return information if the function worked or not along with the regular return value. Basically they return 2 values instead of 1. These are different for asynchronous calls per language so let's focus on synchronous for now.
Various Language Examples of Explicit Returns
Lua functions will throw errors just like Python and JavaScript. However, using a function called protected call, pcall
it will capture the exception as part of a 2nd return value:
function datBoom() error({
reason = 'kapow'
}) end ok, error = pcall(datBoom) print("did it work?", ok, "error reason:", error.reason) --did it work ? false, error : kapow
Go has this functionality natively built in:
func datBoom()(err error) ok, err: = datBoom() if err != nil {
log.Fatal(err)
}
... and so does Elixir (with the ability to opt out using a ! at the end of a function invocation):
def datBoom do
{:error, "kapow"}
end
{:error, reason} = datBoom()
IO.puts "Error: #{reason}" ## kapow
While Python and JavaScript do not have these capabilities built into the language, you can easily add them.
Python can do the same using tuples:
def datBoom():
return (False, 'kapow')
ok, error = datBoom()
print("ok:", ok, "error:", error) # ('ok:', False, 'error:', 'kapow')
JavaScript can do the same using Object destructuring:
const datBoom = () => ({
ok: false,
error: 'kapow'
});
const {
ok,
error
} = datBoom();
console.log("ok:", ok, "error:", error); // ok: false error: kapow
Effects on Coding
This causes a couple of interesting things to happen. First, developers are forced to handle errors when and where they occur. In the throw scenario, you run a lot of code, and sprinkle throws where you think it'll break. Here, even if the functions aren't pure, every single one could possibly fail. There is no point continuing to the next line of code because you already failed at the point of running the function and seeing it failed ( ok
is false) an error was returned telling you why. You start to really think how to architect things differently.
Second, you know WHERE the error occurred (mostly). The "why" is still always up for debate.
Third, and most important, if the functions are pure, they become easier to unit test. Instead of "I get my data, else it possibly blows up", it immediately tells you: "I worked, and here's your data", or "I broke, and here's what could be why".
Fourth, these errors DO NOT (in most cases if your functions are pure) negatively affect the rest of the application. Instead of a throw which could take other functions down with it, you're not throwing, you're simply returning a different value from a function call. The function "worked" and reported its "results". You're not crashing applications just because a function didn't work.
Cons on Explicit Returns
Excluding language specifics (i.e. Go panics, JavaScript's async/await), you have to look in 2 to 3 places to see what went wrong. It's one of the arguments against Node callbacks. People say not to use throw
for control flow, yet all you've done is create a dependable ok
variable. A positive step for sure, but still not a hugely helpful leap. Errors, if detected to be there, mold your code's flow.
For example, let's attempt to parse some JSON in JavaScript. You'll see the absence of a try/catch
replaced with an if(ok === false)
:
const parseJSON = string => {
try {
const data = JSON.parse(string);
return {
ok: true,
data
};
} catch (error) {
return {
ok: false,
error
};
}
};
const {
ok,
error,
data
} = parseJSON(new Date());
if (ok === false) {
console.log("failed:", error);
} else {
console.log("worked:", data);
}
The Either Type
Functions that can return 2 types of values are solved in functional programming by using the Either
type, aka a disjoint union. Typescript (strongly typed language & compiler for JavaScript) supports a psuedo Either as an Algebraic Data Type (aka ADT).
For example, this TypeScript getPerson
function will return Error
or Person
and your compiler helps you with that:
// Notice TypeScript allows you to say 2 possible return values
function getPerson(): Error | Person
The getPerson
will return either Error
, or Person
, but never both.
However, we'll assume, regardless of language, you're concerned with runtime, not compile time. You could be an API developer dealing with JSON from some unknown source, or a front end engineer dealing with user input. In functional programming, they have the concept of a "left or right" in an Either type, or an object depending on your language of choice.
The convention is "Right is Correct" and "Left is Incorrect" (Right is right, Left is wrong).
Many languages already support this in one form or another:
JavaScript through Promises as values: .then
is right, .catch
is left) and Python via deferred values via the Twisted networking engine: addCallback
is right, addErrback
is left.
Either Examples
You can do this using a class or object in Python and JavaScript. We've already shown you the Object version above using {ok: true, data}
for the right, and {ok: false, error}
for the left.
Here's a JavaScript Object Oriented example:
class Either {
constructor(right = undefined, left = undefined) {
this._right = right;
this._left = left;
}
isLeft() {
return this.left !== undefined;
}
isRight() {
return this.right !== undefined;
}
get left() {
return this._left;
}
get right() {
return this._right;
}
}
const datBoom = () => new Either(undefined, new Error('kapow'));
const result = datBoom();
if (result.isLeft()) {
console.log("error:", result.left);
} else {
console.log("data:", result.right);
}
... but you can probably already see how a Promise
is a much better data type (despite it implying async). It's an immutable value, and the methods then
and catch
are already natively there for you. Also, no matter how many then
's or "rights", 1 left can mess up the whole bunch, and it allllll flows down the single catch
function for you. This is where composing Eithers (Promises in this case) is so powerful and helpful.
const datBoom = () => Promise.reject('kapow');
const result = datBoom();
result.then(data => console.log("data:", data)).catch(error => console.log("error:", error));
Pattern Matching
Whether synchronous or not, though, there's a more powerful way to match the Either 'esque types through pattern matching. If you're an OOP developer, think of replacing your:
if ( thingA instanceof ClassA ) {
with:
ClassA: ()=> "it's ClassA",
.
ClassB: ()=> "it's ClassB"
It's like a switch
and case
for types.
Elixir does it with almost all of their functions (the _ being the traditional default
keyword):
case datBoom do
{:ok, data} -> IO.puts "Success: #{data}"
{:error, reason} -> IO.puts "Error: #{reason}"
_ -> IO.puts "No clue, brah..."
end
In JavaScript, you can use the Folktale library.
const datBoom = () => Result.Error('kapow');
const result = datBoom();
const weGood = result.matchWith({
Error: ({
value
}) => "negative...",
Ok: ({
value
}) => "OH YEAH!"
});
console.log("weGood:", weGood); // negative...
Python has pattern matching with Hask (although it's dead project, Coconut is an alternative):
def datBoom():
return Left('kapow')
def weGood(value):
return ~(caseof(value)
| m(Left(m.n)) >> "negative..."
| m(Right(m.n)) >> "OH YEAH!")
result = datBoom()
print("weGood:", weGood(result)) # negative...
Scala does it as well, looking more like a traditional switch statement:
def weGood(value: Either): String = value match {
case Left => "negative..."
case Right => "OH YEAH!"
case _ => "no clue, brah..."
}
weGood(Left('kapow')) // negative...
The Mathematicians came up with Either. Three cool cats at Ericsson in 1986 came up with a different strategy in Erlang: let it crash. Later in 2009, Akka took the same idea for the Java Virtual Machine in Scala and Java.
This flies in the face of the overall narrative of this article: don't intentionally cause crashes. Technically it's a supervised crash. The Erlang / Akka developers know errors are a part of life, so embrace they will happen, and give you a safe environment to react to them happening without bringing down the rest of your application.
It also only becomes relatable if you do the kind of work where uptime with lots of traffic is the number one goal. Erlang (or Elixir) create processes to manage your code. If you know Redux or Elm, the concept of a store to keep your (mostly) immutable data, then you'll understand the concept of a Process in Elixir, and an Actor in Akka. You create a process, and it runs your code.
Except, the framework developers knew that if you find a bug, you'll fix it and upload new code to the server. If the server needs to keep running to serve a lot of customers, then it needs to immediately restart if something crashes. If you upload new code, it needs to restart your new code as the older code processes shut down when they are done (or crash).
So, they created supervisors Elixir| Scala. Instead of creating 1 process that runs your code, it creates 2: one to run your code, and another to supervise it if it crashes, to restart a new one. These processes are uber lightweight (0.5kb of memory in Elixir, 0.3kb in Akka).
While Elixir has support for try, catch, and raise, error handling in Erlang/Elixir is a code smell. Let it crash, the supervisor will restart the process, you can debug the code, upload new code to a running server, and the processes spawned from that point forward will use your new code. This is similar to the immutable infrastructure movement around Docker in Amazon's EC2 Container Service and Kubernetes.
Intentionally crashing programs is a bad programming practice. Using throw
is not the most effective way to isolate program problems, they aren't easy to test, and can break other unrelated things.
Next time you think of using throw
, instead, try doing an explicit return or an Either. Then unit test it. Make it return an error in a larger program and see if it's easier for you to find it given you are the one who caused it. I think you'll find explicit returns or Eithers are easier to debug, easier to unit test, and can lead to better thought out applications.
Published at DZone with permission of James Warden, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments