Dwi Wahyudi

Senior Software Engineer (Ruby, Golang, Java)


In this post we will run-through Golang’s errors features. This is something that we’ll need to take care of when anything goes wrong / unwanted.

Overview

In other programming languages, there are 2 things that they will throw when something goes wrong, Error and Exception. These terms can differ between programming languages. Error cannot be recovered in Java (like OutOfMemoryError) but it is recoverable in Ruby (something like NoMethodError or ZeroDivisionError), Exception is something that can be recovered in Java (IOException, ArrayIndexOutOfBoundsException, etc), but usually not in Ruby (something like NoMemoryError). In those 2 languages, we can write rescuing blocks: try catch (in Java) and begin rescue (in Ruby). Those 2 blocks have the same goal: rescuing from thrown Exception (in Java) or Error (in Ruby).

Error

In Golang we mostly handle errors, and we don’t have any try catch or begin rescue block, because error-handling in Golang is verbose and explicit. We do this by checking if each operation returns error: if err != nil. Call database? Call external api? Opening a file?, etc etc, check if each of them return error. It is very common in Golang to have error as last return value of a method/function, so we expect the error to bubble up. In Golang, errors package is the only way we’ll deal with errors: https://pkg.go.dev/errors

Let say we have a function that check triangle type, it receives 3 sides as the arguments: sideA, sideB and sideC. This function returns an error invalid triangle if there’s any side less than 0.

func checkTriangleType(sideA, sideB, sideC int64) (string, error) {
	if sideA < 0 || sideB < 0 || sideC < 0 {
		return "", errors.New("invalid triangle")
	}

	if sideA == sideB && sideA == sideC {
		return "equilateral", nil
	} else if sideA == sideB || sideA == sideC || sideB == sideC {
		return "isoscele", nil
	} else {
		return "scalene", nil
	}

	return "test", nil
}

As we can see here, we immediately return the error as the last return value. If there’s no error, we just return nil. If there’s an error, we should just ignore any other return values. And this error should bubble up to the call-stack.

	triangleType, err = checkTriangleType(-3, 4, 4)
	if err != nil {
		fmt.Println(err)
	}

For example if the error reaches an http handler we then should check the error type, so we can return approriate response code and response body. We cannot check the err by variable here because errors.New will return different error eventhough the string texts are the same. So errors.New("sample") == errors.New("sample") will return false. So in this case, the comparison can be done by checking the error string. But there’s preferrable way of error checking, which we will see in a minute below.

	triangleType, err = checkTriangleType(-3, 4, 4)
	if err != nil {
		if err.Error() == "invalid triangle" {
			// write something to http response body and header, returns approriate response code (422) and return.
		}

		// write something to http response body and header, returns unknown error code as the last resort and return.
	}

In a simple explanation, when we call errors.New("string text") it basically returns an error with the string value being the field of the error struct. Then the field will be simply returned by Error() method.

Error() method implements the Error() method in Golang error interface as follow:

type error interface {
    Error() string
}

Any struct that implements Error() string method can qualify as error. That’s all.

We can further reuse the errors.New("invalid triangle") by declaring and exposing it as a variable:

  // Somewhere in a new file.
  var InvalidTriangleErr = errors.New("invalid triangle")
  //...

  // change errors.New to use the error variable.
  	if sideA < 0 || sideB < 0 || sideC < 0 {
		return "", InvalidTriangleErr
	}
  //...

  // Update the error handler.
  triangleType, err = checkTriangleType(-3, 4, 4)
	if err != nil {
		if err == InvalidTriangleErr {
			// write something to http response body and header, returns approriate response code (422) and return.
		}

		// write something to http response body and header, returns unknown error code as the last resort and return.
	}

Alternatively we can use errors.Is() method from errors package. https://pkg.go.dev/errors#Is

errors.Is() receive error interface type as the first argument. https://pkg.go.dev/builtin#error Any struct that implements Error() method can be checked.

	triangleType, err = checkTriangleType(-3, -4, 4)
	if err != nil {
		if errors.Is(err, InvalidTriangleErr) {
			// write something to http response body and header, returns approriate response code (422) and return.
		}

		// write something to http response body and header, returns unknown error code as the last resort and return.
	}

But a simple response body of invalid triangle string is not communicative enough to our users, how do we make the error to be more detailed (and still reusable), and how do we check the error type? The answer is below.

Custom Error

As we know, any struct can implement error interface, just by implementing Error() method. Let say we want to verbosely check which side is less than 0. We then create a custom error type: DetailedInvalidTriangleErr, it has the 3 sides as the struct fields, and Error() method will emit string of explanation of which sides are less than 0.

Let’s create the custom error as follow:

type DetailedInvalidTriangleErr struct {
	SideA, SideB, SideC int64
}

func (d *DetailedInvalidTriangleErr) Error() string {
	errMessages := make([]string, 0)

	if d.SideA < 0 {
		errMessages = append(errMessages, fmt.Sprintf("Side A %d is less than 0", d.SideA))
	}

	if d.SideB < 0 {
		errMessages = append(errMessages, fmt.Sprintf("Side B %d is less than 0", d.SideB))
	}

	if d.SideC < 0 {
		errMessages = append(errMessages, fmt.Sprintf("Side C %d is less than 0", d.SideC))
	}

	return strings.Join(errMessages, ", ")
}

Returning and handling the error will be like this:

	if sideA < 0 || sideB < 0 || sideC < 0 {
		return "", &DetailedInvalidTriangleErr{SideA: sideA, SideB: sideB, SideC: sideC}
	}

  // ...
  triangleType, err = checkTriangleType(-3, -4, 4)
	if err != nil {
		var d *DetailedInvalidTriangleErr
		if errors.As(err, &d) {
      // write something to http response body and header, returns approriate response code (422) and return.
      // err.Error() will be like this: invalid triangle, details: %s Side A -3 is less than 0, Side B -4 is less than 0
		}

		// write something to http response body and header, returns unknown error code as the last resort and return.
	}

The method errors.As() will check if error is the matching error type and return true if it matches. https://pkg.go.dev/errors#As

Again it receives error interface as the first parameter, any struct that implements Error() method can fit right in.

Here we check if err is the type DetailedInvalidTriangleErr, if yes, then d will be the err value.

Error Wrapping

Error wrapping is a mechanism to wrap some more informations inside an error variable. To wrap an error. we use fmt.Errorf() method. We can nest the error wrapping too, error wrapping inside an error wrapping, here’s the example:

	if sideA < 0 || sideB < 0 || sideC < 0 {
		err := fmt.Errorf("first wrap: %w", &DetailedInvalidTriangleErr{SideA: sideA, SideB: sideB, SideC: sideC})
    err = fmt.Errorf("second wrap: %w", err)
		return "", err
	}

And here’s how we check the error:

	triangleType, err = checkTriangleType(-3, -4, 4)
	if err != nil {
		var d *DetailedInvalidTriangleErr
		if errors.As(err, &d) {

			// err will be: second wrap: first wrap: Side A -3 is less than 0, Side B -4 is less than 0
		}

    // ...
	}

If we print the error it would

Both errors.Is() and errors.As() will repeatedly unwrap the errors inside the err. No matter how deep we wrap the error inside another error, it will be detected by both methods.

We can manually unwrap the errors by using errors.Unwrap() method like this:

	triangleType, err = checkTriangleType(-3, -4, 4)
	if err != nil {
		err = errors.Unwrap(err)
		var d *DetailedInvalidTriangleErr
		if errors.As(err, &d) {

			// err will be: first wrap: Side A -3 is less than 0, Side B -4 is less than 0
		}

		// ...
	}

Unwrapping will return nil, if there’s no more error to unwrap.

When this article is written Go is still 1.19.

Before 1.20, wrapping multiple errors will return nil value when unwrapping. In 1.20, we can do multiple errors wrapping.

Panic

Errors that we’ve covered so far are things that we can anticipate (we check each operation one by one).

Panic in Golang is different kind of conditions where something can cause the flow of the code to stop, and we as developers might not anticipate it, because it can happen anywhere in the code. For example we might not anticipate if we access an array element by out of bound index.

func main() {
	shouldPanic()
	fmt.Println("I must run")
}

func shouldPanic() {
	numbers := []int{100, 200, 300, 400, 500}
	number := numbers[5]
	fmt.Println(number)
}

Running this code will throw a panic like this: panic: runtime error: index out of range [5] with length 5. fmt.Println("I must run") is not reached because the application is terminated before it reach the line.

Surely enough if this happens in a web server, the web server will be terminated.

The way we can handle this is to recover from the panic, so when something like this happens it won’t terminate our application.

func main() {
	shouldPanic()
	fmt.Println("I must run")
}

func shouldPanic() {
	defer func() {
		if err := recover(); err != nil {
			log.Println("panic occurred:", err)
		}
	}()
	numbers := []int{100, 200, 300, 400, 500}
	number := numbers[5]
	fmt.Println(number)
}

When we run this, panic will be recovered and fmt.Println("I must run") will be executed.

In web server, recovering from panic is very essential, so that’s why some libraries have middlewares/plugins that we must use: