DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Low-Code Development: Leverage low and no code to streamline your workflow so that you can focus on higher priorities.

DZone Security Research: Tell us your top security strategies in 2024, influence our research, and enter for a chance to win $!

Launch your software development career: Dive head first into the SDLC and learn how to build high-quality software and teams.

Open Source Migration Practices and Patterns: Explore key traits of migrating open-source software and its impact on software development.

Related

  • Checking TLS and SSL Versions of Applications in JavaScript, Python, and Other Programming Languages
  • Unleashing the Power of WebAssembly to Herald a New Era in Web Development
  • Revolutionizing Network Operations With Automated Solutions: A Deep Dive Into ReactJS
  • How to Detect VPN Proxies With Python and IP2Location.io API

Trending

  • A Comprehensive Guide To Building and Managing a White-Label Platform
  • Test Smells: Cleaning up Unit Tests
  • Data Governance – Data Privacy and Security – Part 1
  • How To Remove Excel Worksheets Using APIs in Java
  1. DZone
  2. Coding
  3. Languages
  4. Error Handling Strategies

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.

By 
James Warden user avatar
James Warden
·
Nov. 27, 17 · Tutorial
Like (25)
Save
Tweet
Share
17.7K Views

Join the DZone community and get the full member experience.

Join For Free

Introduction

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.

JavaScript dev Python (language) Computer programming

Published at DZone with permission of James Warden, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Checking TLS and SSL Versions of Applications in JavaScript, Python, and Other Programming Languages
  • Unleashing the Power of WebAssembly to Herald a New Era in Web Development
  • Revolutionizing Network Operations With Automated Solutions: A Deep Dive Into ReactJS
  • How to Detect VPN Proxies With Python and IP2Location.io API

Partner Resources


Comments

ABOUT US

  • About DZone
  • Send feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends: