When developing a program, we often face unwanted situations such as abnormal user inputs, a failed database connection, or an attempt to access a file that doesn't exist; these situations are known as errors.
Handling and implementing errors is a way of communicating with both users and other programmers when our program goes into an unexpected state. It also helps us identify and record the diagnostic information to debug and fix our program in the future.
In this topic, we'll learn about errors in Go, how to create custom errors, and the common practice of wrapping errors when we want to give additional context.
What is an error?
Errors indicate an unwanted condition that occurs in our program. Whenever something unexpected happens, we get an error. Go has embedded support for errors that works in a simple way: functions return errors as their last return value. In layperson's terms, that means that we can check the error immediately before proceeding to the next steps of the function.
In Go, many functions of packages contained in the Standard library have predefined errors. Let's take a look at an example of a predefined error occurring when trying to open a non-existing file using the os.Open function:
package main
import (
"log"
"os"
)
func main() {
// Try to open a non-existing file: "new_file.txt"
file, err := os.Open("new_file.txt") // os.Open returns two values: the file and an error
if err != nil {
log.Fatal(err) // Log the error & exit the program with the exit code 1 - meaning: general error
}
defer file.Close() // This line closes the file before exiting the program
}
// Output:
// 2022/10/04 06:09:55 open new_file.txt: The system cannot find the file specified.In the code snippet above, the variable err is assigned to the second return value of the os.Open function, which is an error type value; error is an interface type declared in Go's builtin package that returns a string representation of the error message, and if there is no error, it returns nil.
In this example, since new_file.txt doesn't exist, the value of err becomes the following error value: "open new_file.txt: The system cannot find the file specified.". When compared to nil, it evaluates to true, thus logging the error message into our terminal and finally exiting the program with the exit code 1 — general error.
Creating custom errors
Go's Standard library offers two functions to create errors: errors.New from the errors package and fmt.Errorf from the fmt package. The difference between them is that fmt.Errorf provides the ability to add formatting to our error message, which means we can pass a parameter to the fmt.Errorf function and include it in the error message.
Suppose we have the divide function in our Go program, and we want to show an error message when the user tries to divide a number by zero. Let's take a look at the implementation of the error message when using theerrors.New and the fmt.Errorf functions:
// Implementation with errors.New
func divide (num1, num2 float64) (float64, error) {
if num2 == 0 {
return 0, errors.New("cannot divide by zero")
}
return num1 / num2, nil
}
// Output: cannot divide by zero// Implementation with fmt.Errorf
func divide (num1, num2 float64) (float64, error) {
if num2 == 0 {
return 0, fmt.Errorf("cannot divide %.2f by %.2f", num1, num2)
}
return num1 / num2, nil
}
// Output: cannot divide 10.00 by 0.00We can clearly see the difference between the two outputs. When using errors.New, we can only output a string as the error message. However, when using fmt.Errorf, we can add additional information to the error message by passing num1 and num2 as parameters, so the user knows what numeric values are within each variable.
Wrapping errors
In Go, when a function returns an error, it's common for the calling function to add more context by wrapping the error. This approach is often used to provide clear information on where in our program the error originated.
We can create wrapped errors by using the %w verb with the fmt.Errorf function. Apart from that, we can also output the original error or unwrapped error via the errors.Unwrap function. Let's take a look at the implementation of both functions in a Go program:
package main
import (
"errors"
"fmt"
"os"
)
// The function openFile returns a custom error message if opening the file fails
func openFile(filename string) error {
if _, err := os.ReadFile(filename); err != nil {
return fmt.Errorf("error opening %s: %w", filename, err)
}
return nil
}
func main() {
err := openFile("new_file.txt")
if err != nil {
fmt.Printf("error running main.go: %s\n", err) // Print the wrapped error message
unwrappedErr := errors.Unwrap(err) // This line unwraps the error
fmt.Printf("unwrapped error: %s\n", unwrappedErr) // Print the original error message
}
}After executing this program, we will have the following output:
error running main.go: error opening new_file.txt: open new_file.txt: The system cannot find the file specified.
unwrapped error: open new_file.txt: The system cannot find the file specified.The first line is the output of a context error message followed by the wrapped error.
It first prints a context message: "error running main.go"; after that, it prints the custom error: "error opening new_file.txt" created within the openFile function. Finally, it prints the error thrown by the Go compiler: "open new_file.txt: The system cannot find the file specified."
In turn, the second line outputs a context error message followed by the unwrapped error.
It first prints a context message: "unwrapped error:", followed by unwrappedErr, which only contains the error thrown by the Go compiler: "open new_file.txt: The system cannot find the file specified."
Summary
In this topic, we've learned what an error in Go is, how to print predefined errors from packages, how to create our custom errors, and how to give additional error context by wrapping consecutive error messages together.
Knowing how to properly implement errors in a Go program is a valuable skill. By employing the error handling techniques we've covered in this topic, we'll be able to write more reliable and succinct Go code. Way to go!