Wrapping Multiple Errors in Golang
Error handling and management have always been a hot topic for debate in Golang. Software developers coming from different programming background has their own opinionated view of how it should be done. Many packages have been born to suggest the “correct” way of handling errors, such as pkg/errors.
In Go 1.13, the Go team has launched the official way on how errors should be use. However this did not stop the debate as we have seen endless new proposals for error handling. There were also gaps left to be filled as old problems such as wrapping multiple errors are not addressed by the official library. In similar fashion, there are packages aim to deal with multi errors such as go-multierror by Hashicorp and multierr package by Uber
In Go 1.20 this gap will be filled as the Go team finally release the official way to handle multiple errors.
Wrapping errors
Wrapping errors allows us to embed errors into another error. It allows us to add additional information to the error while retaining the original error. Since Go 1.13, we can do the following to wrap the error.
import (
"errors"
"fmt"
)
var notFound = errors.New("NotFound")
func CheckFileExist(filename string) error {
return fmt.Errorf("failed to find file %s: %w", filename, notFound)
}
Notice the format verb %w
? By specifying %w
instead of %v
, we are wrapping the error so the original error can still be obtained by unwrapping it when necessary.
Why do we add information to errors?
Now suppose you want to move 2 files together to a new directory
import (
"fmt"
"os"
)
func MoveBothFiles(firstFileSrc, firstFileDest, secFileSrc, secFileDest string) error {
// Move the first file from src to dest
if err := os.Rename(firstFileSrc, firstFileDest); err != nil {
return fmt.Errorf("failed to move first file from %s to %s: %w", firstFileSrc, firstFileDest, err)
}
// Move the second file from src to dest
if err := os.Rename(secFileSrc, secFileDest); err != nil {
return fmt.Errorf("failed to move second file from %s to %s: %w", secFileSrc, secFileDest, err)
}
return nil
}
By adding additional information, you are able to differentiate the failure of the move operation occurs on which file while preserving the original cause of the error from the os
package.
Multiple Errors
Since we want to move both files, the function should ensure that both files are move successfully or none of them are moved. So we revert the move operation of the first file should the operation of the second file fails.
import (
"fmt"
"os"
)
func MoveBothFiles(firstFileSrc, firstFileDest, secFileSrc, secFileDest string) error {
// Move the first file from src to dest
if err := os.Rename(firstFileSrc, firstFileDest); err != nil {
return fmt.Errorf("failed to move first file from %s to %s: %w", firstFileSrc, firstFileDest, err)
}
// Move the second file from src to dest
if err := os.Rename(secFileSrc, secFileDest); err != nil {
// Revert the move operation of the first file
if revertErr := os.Rename(firstFileDest, firstFileSrc); err != nil {
err = fmt.Errorf("failed to revert first file %v: %w", revertErr, err)
}
return fmt.Errorf("failed to move second file from %s to %s: %w", secFileSrc, secFileDest, err)
}
return nil
}
Are you able to spot the problem we have now with wrapping errors? In this line:
err = fmt.Errorf("failed to revert first file %v: %w", revertErr, err)
You can see that we have multiple errors, but we are only able to wrap and preserve one error. This is because before Go 1.20, fmt.Errorf()
is able to support only one occurrence of %w
format verb. The error formatted with %s
will no longer be tracked and normalized as only an additional information.
Go 1.20
With Go 1.20, we now can handle multiple errors and wrap both the errors.
import (
"errors"
"fmt"
"os"
"syscall"
)
func MoveBothFiles(firstFileSrc, firstFileDest, secFileSrc, secFileDest string) error {
// Move the first file from src to dest
if err := os.Rename(firstFileSrc, firstFileDest); err != nil {
return fmt.Errorf("failed to move first file from %s to %s: %w", firstFileSrc, firstFileDest, err)
}
// Move the second file from src to dest
if err := os.Rename(secFileSrc, secFileDest); err != nil {
// Revert the move operation of the first file
if revertErr := os.Rename(firstFileDest, firstFileSrc); err != nil {
err = fmt.Errorf("failed to revert first file %w: %w", revertErr, err)
}
return fmt.Errorf("failed to move second file from %s to %s: %w", secFileSrc, secFileDest, err)
}
return nil
}
func main() {
err := MoveBothFiles(
"/path/to/first/src",
"/path/to/first/dest",
"/path/to/second/src",
"/path/to/second/dest",
)
// This will check if one of the errors is due to syscall.EEXIST
if errors.Is(err, syscall.EEXIST) {
// Do something...
}
// This will find the first error in the error tree that matches os.*PathError
var pathError *os.PathError
if errors.As(err, &pathError) {
// Do something...
}
}
By having multiple %w
, the function fmt.Errorf()
will instead create a list of errors. Underneath the hood, it will return error[]
instead of error
. Unwrapping such error would returns the list error[]
, allowing the inspection of every wrapped errors.
Wrapping multiple errors without additional information
If there is no desire to add additional information, we can easily wrap all errors using the newly introduced function errors.Join()
. This is demonstrated by the official go documentation
import (
"errors"
"fmt"
)
func main() {
err1 := errors.New("err1")
err2 := errors.New("err2")
err := errors.Join(err1, err2)
fmt.Println(err)
if errors.Is(err, err1) {
fmt.Println("err is err1")
}
if errors.Is(err, err2) {
fmt.Println("err is err2")
}
}
You May Also Like
When a Golang nil error is not nil
Coding in Golang are usually fun and easy for developers that are new to the language. There are sometimes gotcha which will catch out even experience …
Read ArticleCrash Course on Golang Benchmarks: A Beginner's Perspective
Golang, renowned for its simplicity and efficiency, employs benchmark testing as a fundamental tool for performance evaluation. In this exploration, …
Read Article