How to Handle Errors in Go
Language
- unknown
by James Smith (Golang Project Structure Admin)
Even the best-written code will occasionally run into problems. If hardware breaks, network connections are lost or users provide unexpected input, errors make us aware of the problem and allow us to take action to fix it.
We will begin by discussing how errors are defined in Go and then we will go through some examples of working with error-handling techniques.
Table of Contents
What Is an Error?
Errors reflect an abnormal, problematic or unexpected state occurring in a program’s runtime.
In Go, errors are generally specified using the standard interface below:
type error interface {
Error() string
}
In other words, any type that has an Error
method that returns a string, giving a human-readable description of the problem, will fulfil the requirements of the error
interface.
How to Create Custom Types to Act as Errors
Given what we’ve said above about the simplicity of the native error
interface, it’s easy to create your own type that implements it.
The example below shows a custom error being defined and then passed to the builtin panic
function:
package main
type printerError struct{}
func (p printerError) Error() string {
return "cannot access printing device"
}
func main() {
panic(printerError{})
}
This is a very simple example, since the Error
method always returns the same message. In real-world code, the printerError
struct would perhaps contain more fields that determine the exact type of problem that has occurred with the computer’s printing device and the string returned by the Error
method would differ depending on what went wrong.
What Is the Panic Function?
In some programming languages (such as Java, for example), we talk of “throwing exceptions”. We don’t use that terminology in Go, but calling the panic
function with a type that implements the error
interface will cause the program to terminate in much the same way.
It is also possible to call the panic function with a string directly (or any value that can be converted by the compiler to a string), which will act as the error message.
Running the code in the example we looked at above, for example, produces this output (using the stderr stream on Linux):
panic: cannot access printing device
goroutine 1 [running]:
main.main()
/tmp/sandbox2991161662/prog.go:10 +0x27
Program exited.
You can see that the output includes the custom message we defined. However, it also includes information about the line number within our Go file where the panic
function was called (the tenth line down) and the number identifying the goroutine (the first — and, in this case, only — one).
The program immediately terminates whenever a call to panic
is encountered, so any code that follows the call will not run.
However, functions that have been registered with the defer
keyword will run as the program exits. This can be seen in the example below:
package main
import "fmt"
func helperFunction() {
defer fmt.Println("this will print [1]")
panic("stop")
fmt.Println("this won't print [1]")
}
func main() {
defer fmt.Println("this will print [2]")
helperFunction()
fmt.Println("this won't print [2]")
}
The code in the two defer
statements will run before the helperFunction
and main
functions return, allowing the program to terminate.
This can be seen in the program’s output:
this will print [1]
this will print [2]
panic: stop
goroutine 1 [running]:
main.helperFunction()
/tmp/sandbox3562109774/prog.go:8 +0x73
main.main()
/tmp/sandbox3562109774/prog.go:16 +0x70
Program exited.
Notice how defer
functions always get called in an order opposite to the one in which they’re declared.
How to Create Custom Errors Using the Standard Library
If you ever want to create a custom type that implements the error
interface and only ever returns the same string in its Error
method, as we did with the printerError
that we defined earlier, then the Go standard library has an easier way to do this.
The errors.New
function takes a single string, that will act as the error message, and returns a value that will comply with the error
interface. Note that if you call the errors.New
function twice with the same string, it will return distinct values, although both will contain the same message.
Taking this newfound knowledge into consideration, we could have written the previous example much more simply like so:
package main
import "errors"
func main() {
panic(errors.New("cannot access printing device"))
}
The fmt.Errorf
function works in a similar way, returning a custom error value, while allowing you to use a format specifier (identical to the one used in the fmt.Printf
function) in order to create the error-message string.
Here is an example of this function being used to generate an error:
package main
import "fmt"
func main() {
const userAge = 19
const minAge = 21
if userAge < minAge {
panic(fmt.Errorf("the user's age (%d) is below %d, so access has been denied", userAge, minAge))
}
fmt.Println("access granted")
}
You can also see above how conditional code is used to check if a certain restricted or dangerous circumstance has occurred before we call the panic
function. This is usually how we would write real-world code, since we should only ever want to stop the program if it’s absolutely necessary.
If you ever find yourself calling panic
outside of a conditional block, there may be something fundamentally wrote with your code — unless you are doing it temporarily simply for the purposes of testing your error-handling.
We can see in the output produced by the example above that the two numbers, userAge
and minAge
, have been correctly interpolated into the error message:
panic: the user's age (19) is below 21, so access has been denied
goroutine 1 [running]:
main.main()
/tmp/sandbox2479504225/prog.go:10 +0x75
Program exited.
Returning Errors From a Function
Of course, as mentioned earlier, calling panic
should be considered a very extreme way to deal with an error, since it stops the entire program and ensures that no other work will be done.
Generally, if an error can be handled or corrected in some way, this is the approach that should be taken. Only if an error is mission-critical should it need to cause the entire program to come to a sudden halt.
If you’re writing a function that performs a specific job, it is good practice to return an error (in addition to as any other return values), so that the user who calls the function can decide for themselves how they want to handle it. This is true even if you’re going to be only person who calls the function, since you may not always want to call it in the same way.
Look at the following example function below:
package main
import "errors"
var (
validUserIds = []int{4, 9, 32, 91, 194, 201}
)
func userIsValid(userId int) (bool, error) {
if userId < 0 {
return false, errors.New("userId cannot be negative")
}
for _, validUserId := range validUserIds {
if userId == validUserId {
return true, nil
}
}
return false, nil
}
It takes an ID number to determine if there is a valid user in the system associated with that ID. The function will iterate through all of the valid user IDs, and if it finds a match, it will return true, otherwise false.
However, if the ID passed as an argument to the function is a negative number, the function will also return an error value, since it’s assumed that all IDs must be positive.
We could simply have returned false
in this case, but by returning an error, we are allowing the person who calls this function to make a choice about how to handle the unexpected input: perhaps another ID could be requested, perhaps the current ID could be multiplied by -1
to make it positive and the function called again or perhaps panic
will be called to bring the entire program to a halt.
But the important point is that each of these choices, and others, are potentially valid, so it is important not to restrict other developers’ choices by handing errors within your own functions. Always return the error so that they can decide how best to handle it for themselves.
You will probably have noticed that this is how the Go standard library deals with errors. Look, for example, at the example below:
package main
import (
"os"
)
func main() {
file, err := os.Open("/dev/random")
if err != nil {
panic(err)
}
buffer := make([]byte, 8)
if _, err = file.Read(buffer); err != nil {
panic(err)
}
fmt.Println(buffer)
}
We are reading from the /dev/random file, which on UNIX-based machines acts as a pseudorandom number generator. It is a special file, because every time we read from it, its contents will appear to contain different pseudorandom bytes.
Both the os.Open
and file.Read
functions provide an error as well as their primary return value. This allows us to decide how we want to handle any system malfunctions; perhaps, if /dev/random is not available, we might choose to use an alternative source of pseudorandom data, for example, or simply use some non-random data instead.
Since, in the example above, we simply call panic whenever we receive an error, we could create a helper function to deal with the error-handling for us:
package main
import (
"fmt"
"os"
)
func checkError(err error) {
if err != nil {
panic(err)
}
}
func main() {
file, err := os.Open("/dev/random")
checkError(err)
buffer := make([]byte, 8)
_, err = file.Read(buffer)
checkError(err)
fmt.Println(buffer)
}
The checkError
function removes a lot of the repetitive boilerplate from our code. However, we should try not to rely heavily on such functions, since we are giving up the advantage of being able to choose to handle different errors differently in different situations.