Learn Go: Creating Custom Errors in Golang #35

This tutorial deals with how to create our own custom errors which we can use in functions and packages

Gopher

Lets go!!

Published · Feb 20 2020

Previous Tutorial: Handling Errors in Golang #34
This tutorial deals with how to create our own custom errors which we can use in functions and packages we create. We will also use the same techniques employed by the standard library to provide more details about our custom errors.
. . .

Introduction

Go provides two methods to create errors in the standard library, errors.New and fmt.Errorf. When communicating more complicated error information to your users, or to your future self when debugging, sometimes these two mechanisms are not enough to adequately capture and report what has happened.

To convey this more complex error information and attain more functionality, we can implement the standard library interface type, error.

The syntax for this would be as follows:
type error interface { Error() string }

The builtin package defines error as an interface with a single Error() method that returns an error message as a string. By implementing this method, we can transform any type we define into an error of our own.
. . .

Creating custom errors using the New function

The simplest way to create a custom error is to use the New function of the errors package.

Before we use the New function to create a custom error, let's understand how it is implemented. The implementation of the New function in the errors package is provided below.
// Package errors implements functions to manipulate errors. package errors // New returns an error that formats as the given text. func New(text string) error { return &errorString{text} } // errorString is a trivial implementation of error. type errorString struct { s string } func (e *errorString) Error() string { return e.s }

The implementation is pretty simple. errorString is a struct type with a single string field s. The Error() string method of the error interface is implemented using a errorString pointer receiver in line no. 14.

The New function in line no. 5 takes a string parameter, creates a value of type errorString using that parameter and returns the address of it. Thus a new error is created and returned.

Now that we know how the New function works, lets use it in a program of our own to create a custom error.

We will create a simple program which calculates the area of a circle and will return an error if the radius is negative.
package main import ( "errors" "fmt" "math" ) func circleArea(radius float64) (float64, error) { if radius < 0 { return 0, errors.New("Area calculation failed, radius is less than zero") } return math.Pi * radius * radius, nil } func main() { radius := -20.0 area, err := circleArea(radius) if err != nil { fmt.Println(err) return } fmt.Printf("Area of circle %0.2f", area) }


In the program above, we check whether the radius is less than zero in line no. 10. If so we return zero for the area along with the corresponding error message. If the radius is greater than 0, then the area is calculated and nil is returned as the error in line no. 13.

In the main function, we check whether the error is not nil in line no. 19. If it's not nil, we print the error and return, else the area of the circle is printed.

In this program the radius is less than zero and hence it will print:
Area calculation failed, radius is less than zero
. . .

Creating custom errors using the fmt.Errorf

The above program works well but wouldn't it be nice if we print the actual radius which caused the error.

This is where the Errorf function of the fmt package comes in handy. This function formats the error according to a format specifier and returns a string as value that satisfies error.

Let's use the Errorf function and make the program better.
package main import ( "fmt" "math" ) func circleArea(radius float64) (float64, error) { if radius < 0 { return 0, fmt.Errorf("Area calculation failed, radius %0.2f is less than zero", radius) } return math.Pi * radius * radius, nil } func main() { radius := -20.0 area, err := circleArea(radius) if err != nil { fmt.Println(err) return } fmt.Printf("Area of circle %0.2f", area) }


In the program above, the Errorf is used in line no. 10 to print the actual radius which caused the error. Running this program will output:
Area calculation failed, radius -20.00 is less than zero
. . .

Custom Error Struct

Sometimes a custom error is the cleanest way to capture detailed error information.

For example, let’s say we want to capture the status code for errors produced by an HTTP request; run the following program to see an implementation of error that allows us to cleanly capture that information:
package main import ( "errors" "fmt" "os" ) type RequestError struct { StatusCode int Err error } func (r *RequestError) Error() string { return fmt.Sprintf("status %d: err %v", r.StatusCode, r.Err) } func doRequest() error { return &RequestError{ StatusCode: 503, Err: errors.New("unavailable"), } } func main() { err := doRequest() if err != nil { fmt.Println(err) os.Exit(1) } fmt.Println("success!") }


We will see the following output:
Output status 503: err unavailable exit status 1

In this example, we create a new instance of RequestError and provide the status code and an error using the errors.New function from the standard library. We then print this using fmt.Println as in previous examples.

Within the Error() method of RequestError, we use the fmt.Sprintf function to construct a string using the information provided when the error was created.
. . .

Wrapping Errors

Commonly, an error will be generated from something outside of your program such as: a database, a network connection, etc. The error messages provided from these errors don’t help anyone find the origin of the error.

Wrapping errors with extra information at the beginning of an error message would provide some needed context for successful debugging.

The following example demonstrates how we can attach some contextual information to an otherwise cryptic error returned from some other function:
package main import ( "errors" "fmt" ) type WrappedError struct { Context string Err error } func (w *WrappedError) Error() string { return fmt.Sprintf("%s: %v", w.Context, w.Err) } func Wrap(err error, info string) *WrappedError { return &WrappedError{ Context: info, Err: err, } } func main() { err := errors.New("boom!") err = Wrap(err, "main") fmt.Println(err) }


We will see the following output:
Output main: boom!

WrappedError is a struct with two fields: a context message as a string, and an error that this WrappedError is providing more information about. When the Error() method is invoked, we again use fmt.Sprintf to print the context message, then the error (fmt.Sprintf knows to implicitly call the Error() method as well).

Within main(), we create an error using errors.New, and then we wrap that error using the Wrap function we defined. This allows us to indicate that this error was generated in "main". Also, since our WrappedError is also an error, we could wrap other WrappedErrors—this would allow us to see a chain to help us track down the source of the error. With a little help from the standard library, we can even embed complete stack traces in our errors.
. . .

Conclusion

Since the error interface is only a single method, we’ve seen that we have great flexibility in providing different types of errors for different situations.

While the error handling mechanisms in Go might on the surface seem simplistic, we can achieve quite rich handling using these custom errors to handle both common and uncommon situations.

Go has another mechanism to communicate unexpected behavior, panics. In our next article in the error handling series, we will examine panics—what they are and how to handle them.
. . .
Next Tutorial: Panic and Recover in Golang #36

. . .

On a mission to build Next-Gen Community Platform for Developers