Golang vs Javascript Error Handling

·

9 min read

A few months ago, I had to make a switch from Javascript being my primary language to Golang. This series is meant to discuss some of the differences I noticed between both languages. Note that I am using Typescript-flavored Javascript, you should too.

This Article covers an area that was a major switch for me between writing JS and Golang programs, Errors and Error Handling. All programmers have bugs in their code except you're a TV hacker, who writes 1000 lines of code and it runs the first time without issues. The rest of us always run into errors, syntax errors and runtime errors.

In this article the primary focus is on handling runtime errors, these are errors that show face in our code while it is running. Everything seems to be going smoothly and then boom, something fails and the whole system goes spiralling down. Runtime errors can be caused by wrong input by users of our service, failure of third-party services, invalid memory access and database malfunctions amongst others. How then do we deal with these system crashing errors effectively?

Why should you care about error handling?

Before we dive into error handling in both languages, I have to make a case for why you should handle your errors gracefully. Most of the time, you are building tools and services that will be used by third parties, and it would be very out of place for your entire application to come crashing down because someone sent in a string instead of a number. It is good practice to set up your systems in a resilient way such that errors are handled effectively and cannot bring down the system abruptly. Also, proper error handling ensures that users of your service know how to act in certain scenarios and are not left to blindly figure stuff out on their own.

Proper error handling generally involves intercepting the error, communicating the error with the users of your service and finally acting on the error, this could involve logging it or in some extreme cases even shutting off the service because of the error.

Exceptions vs Errors

To understand error handling in both JS and Go we must first know the distinction between errors and exceptions. A Javascript error is an object that is an instance of the Error class, in Golang an error implements the error interface. Essentially they are the same thing, and the implication of this is that errors are simply values. The way you could create an instance of a class in JS, play with it and manipulate it how you like, you can do the same with an error object.

const typeMisMatchError = new Error('Type Mismatch Error')
var TypeMisMatchError = errors.New('type mismatch error')

An exception, on the other hand, is an event that disrupts the normal flow of the program. Exceptions represent an abnormality in the program and when a program encounters any abnormalities the default action is to stop all processes and hand over the exception to the runtime. The runtime attempts to find something to handle the exception. The runtime searches up the scope starting from the caller of the function and it keeps going up the scope, and if there is no mechanism to handle the error the program terminates. The distinction between error values and exceptions will come in handy later.

Error Handling

There are two different approaches to error handling as I have seen in Golang and Javascript. In Javascript, errors are communicated using exceptions, the minute your program runs into an error you throw an exception. This exception is then bubbled up until it is handled further up the stack. It is important to note that once an exception is encountered, the execution of that code piece stops and if there is no mechanism for handling the errors the code breaks.

In Golang on the other hand, errors are generally returned as values. Golang allows for multiple return values from a function and the standard is to return errors as the final return value from a function, such that when a piece of code encounters an error, rather than stopping execution at that point, it simply returns an error value to the caller of the function who then decides what can be done with the error. The important distinction here is that errors returned from a function call in Golang do not stop the execution of the calling function, while JS exceptions thrown in a function will immediately break the code and stop other code blocks from executing except error handling measures are put in place (discussed later).

These concepts of error handling are baked into the standard library packages of both languages and can be demonstrated in the code snippets below.

const http = require("http") 

http.get("", data => console.log(data)) //Throws an exception
console.log("We Got Here") //This line is not reached
package main

import (
    "fmt"
  "net/http"
)

func main() {
    data, _ := http.Get("")
    fmt.Println(data) //nil is logged to the console
    fmt.Println("We Got Here") //"We got Here" is logged to the console
}

The blocks of code above do the same thing,

  • Import the native package for making HTTP requests

  • Make a HTTP GET request

  • Print the result to the console

  • Print the string "We Got Here" to the console.

The catch here is that no URL is provided in the GET request. What do you think happens here?

The JS code breaks immediately and an exception is logged to the console. The final line of code in the JS script is never run as the code terminates prematurely. On the other hand, the Go code runs into an error in the GET request an error is returned to the caller of the Get request and the code proceeds as usual. Since no data was obtained from the get request it prints nil to the console and finally prints the string snippet before exiting.

The way error values are communicated between code blocks in both languages is very different. Now to the issue of handling the errors. One can get tempted to ignore errors in Go, simply because they do not crash the program immediately but it is important to note that this brings lots of uncertainty into your code. In the code block above, the data object has a nil value, if it was to be used for any other processing or even returned to a client, a bug has been introduced into the program. Hence in Golang, errors are usually handled as they occur. The convention in Golang is to check if the returned error value is equal to nil. A nil value implies that no error was encountered during the request and the runtime can proceed with the rest of the program.

func main() {
    data, err := http.Get("")
    if err != nil { //Inspecting the error value incase its not nil
        os.Exit(1) // If error != nil, the code exits
    }
    //This line of code is never reached if error is not nil
    fmt.Println(data) 
}

In JS, the default approach is to throw errors when they occur and use try-catch blocks to intercept and deal with the errors. Exceptions are the bubbled up to the caller of a function, if there is no mechanism to intercept the error, it is further bubbled up the scope till it finally gets to the point where it breaks the code or it is picked up by a global error listener. It is good practice to set up a global listener such that in cases where an error happens to slip through it will be intercepted and a nice error message will be communicated to the users of the program.

const http = require("http") 
try {
    http.get("", data => console.log(data)) //Throws an exception
}
catch(error) {
    process.exit(1) // Exits the code if there is an error
}

Both approaches to error handling in both languages have their merits and demerits. My primary issue with the JS try-catch system is the fact that it can get clutter up the codebase very fast. For example, your system has four API calls to make, and each of the API calls can potentially fail hence you have to provide some mechanism to deal with errors when they happen. You could wrap all the function calls into one try-catch block, hence any errors that happen in any of the calls will be caught in the catch block. The problem with this approach is that you would have to carry out some extra processing to determine which of the API calls was responsible for the error.

const http = require("http") 
try {
    http.get("", data => console.log(data)) //Logs the data
    http.get("", data => console.log(data)) //Logs the data
    someDBCall() //Throws exceptions could not connect to db
} catch(error) {
    process.exit(1) // Exits the code if there is an error
}

Alternatively, you could have a try-catch block for each of the API calls, but this implies an additional five to six lines of code per API call which will make the codebase appear unnecessarily cluttered.

const http = require("http") 
try {
    http.get("", data => console.log(data)) 
} catch(error) {
    process.exit(1) 
}
try {
    someDBCall() 
} catch(error) {
    process.exit(1) // Exits the code if there is an error
}

In Golang, immediately after a function is called we evaluate the error value to ensure that there were no errors. This implies that for each function call, we will have a couple of lines for error handling. This would clutter up the codebase as there is error handling everywhere. In certain scenarios, you might even have more lines of code for error handling than you have for doing the core business logic. The code block below shows a common sight in Golang codebases.

func main() {
    data, err := http.Get("")
    if err != nil {
        os.Exit(1) 
    }
    v, err := someDBCall() 
        if err != nil {
        os.Exit(1) 
    }
}

Also, it is easy to ignore Golang errors as they do not break the code immediately. As Golang errors are returned as values and not thrown as exceptions, it is very easy to ignore them as they do not cause the code to stop execution immediately. It is also important to note that the concept of exceptions also exists in Go through the use of Panics although this concept is reserved for extreme cases. You rarely ever need to panic in your code.

Errors in Go and JS are essentially the same, but the procedures for handling errors are different and for someone making the switch between the languages, this is something that might take a little bit of getting used to.

My next article in this series will be comparing asynchronous programming in NodeJs and Golang.

References

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Control_flow_and_error_handling

https://docs.oracle.com/javase/tutorial/essential/exceptions/index.html

https://dave.cheney.net/2012/01/18/why-go-gets-exceptions-right