I wondering what is the best way to handle error form multiple level abstraction in go. Every time if I must add a new level abstraction to program, I am forced to transfer error code from level less to level high. Thereby is duplicate communitaces in log file or I must remmember to delete communicate form level low and transfer him to level higher. Below simply example. I skipped creating each object to more shortly and celar code, but I think You understand my problem
type ObjectOne struct{
someValue int
}
func (o* ObjectOne)CheckValue()error{
if o.someValue == 0 {
SomeLogger.Printf("Value is 0 error program") // communicate form first level abstraction to logger
return errors.New("Internal value in object is 0")
}
return nil
}
type ObjectTwoHigherLevel struct{
objectOne ObjectOne
}
func (oT* ObjectTwoHigherLevel)CheckObjectOneIsReady() error{
if err := oT.objectOne.CheckValue() ; err != nil{
SomeLogger.Printf("Value in objectOne is not correct for objectTwo %s" , err) // second communicate
return err
}
return nil
}
type ObjectThreeHiggerLevel struct{
oT ObjectTwoHigherLevel
}
func (oTh* ObjectThreeHiggerLevel)CheckObjectTwoIsReady()error{
if err := oTh.oT.CheckObjectOneIsReady() ; err != nil{
SomeLogger.Printf("Value in objectTwo is not correct for objectThree %s" , err)
return err
}
return nil
}
In result in log file I get duplicate posts
Value is 0 error program
Value in objectOne is not correct for objectTwo Internal value in object is 0
Value in objectTwo is not correct for objectThree Internal value in object is 0
In turn if I only transfer some err to higher level without additional log I lost information what happend in each level.
How this solve ? How privent duplicate communicates ? Or My way is the good and the only ?
Problem is more frustrating if I create a few object which search something in database on a few abstraction level then I get also few lines form this same task in logFile.
EDIT: This answer pre-dates Go 1.13 which provides something similar to the presented technique. Please check The Go Blog: Working with Errors in Go 1.13.
You should either handle an error, or not handle it but delegate it to a higher level (to the caller). Handling the error and returning it is bad practice as if the caller also does the same, the error might get handled several times.
Handling an error means inspecting it and making a decision based on that, which may be you simply log it, but that also counts as "handling" it.
If you choose to not handle but delegate it to a higher level, that may be perfectly fine, but don't just return the error value you got, as it may be meaningless to the caller without context.
Annotating errors
A really nice and recommended way of delegation is Annotating errors. This means you create and return a new error value, but the old one is also wrapped in the returned value. The wrapper provides the context for the wrapped error.
There is a public library for annotating errors: github.com/pkg/errors; and its godoc: errors
It basically has 2 functions: 1 for wrapping an existing error:
func Wrap(cause error, message string) error
And one for extracting a wrapped error:
func Cause(err error) error
Using these, this is how your error handling may look like:
func (o *ObjectOne) CheckValue() error {
if o.someValue == 0 {
return errors.New("Object1 illegal state: value is 0")
}
return nil
}
And the second level:
func (oT *ObjectTwoHigherLevel) CheckObjectOneIsReady() error {
if err := oT.objectOne.CheckValue(); err != nil {
return errors.Wrap(err, "Object2 illegal state: Object1 is invalid")
}
return nil
}
And the third level: call only the 2nd level check:
func (oTh *ObjectThreeHiggerLevel) CheckObjectTwoIsReady() error {
if err := oTh.ObjectTwoHigherLevel.CheckObjectOneIsReady(); err != nil {
return errors.Wrap(err, "Object3 illegal state: Object2 is invalid")
}
return nil
}
Note that since the CheckXX() methods do not handle the errors, they don't log anything. They are delegating annotated errors.
If someone using ObjectThreeHiggerLevel decides to handle the error:
o3 := &ObjectThreeHiggerLevel{}
if err := o3.CheckObjectTwoIsReady(); err != nil {
fmt.Println(err)
}
The following nice output will be presented:
Object3 illegal state: Object2 is invalid: Object2 illegal state: Object1 is invalid: Object1 illegal state: value is 0
There is no pollution of multiple logs, and all the details and context are preserved because we used errors.Wrap() which produces an error value which formats to a string which preserves the wrapped errors, recursively: the error stack.
You can read more about this technique in blog post:
Dave Cheney: Don’t just check errors, handle them gracefully
"Extending" errors
If you like things simpler and / or you don't want to hassle with external libraries and you're fine with not being able to extract the original error (the exact error value, not the error string which you can), then you may simply extend the error with the context and return this new, extended error.
Extending an error is easiest done by using fmt.Errorf() which allows you to create a "nice" formatted error message, and it returns you a value of type error so you can directly return that.
Using fmt.Errorf(), this is how your error handling may look like:
func (o *ObjectOne) CheckValue() error {
if o.someValue == 0 {
return fmt.Errorf("Object1 illegal state: value is %d", o.someValue)
}
return nil
}
And the second level:
func (oT *ObjectTwoHigherLevel) CheckObjectOneIsReady() error {
if err := oT.objectOne.CheckValue(); err != nil {
return fmt.Errorf("Object2 illegal state: %v", err)
}
return nil
}
And the third level: call only the 2nd level check:
func (oTh *ObjectThreeHiggerLevel) CheckObjectTwoIsReady() error {
if err := oTh.ObjectTwoHigherLevel.CheckObjectOneIsReady(); err != nil {
return fmt.Errorf("Object3 illegal state: %v", err)
}
return nil
}
And the following error message would be presented at ObjectThreeHiggerLevel should it decide to "handle" it:
o3 := &ObjectThreeHiggerLevel{}
if err := o3.CheckObjectTwoIsReady(); err != nil {
fmt.Println(err)
}
The following nice output will be presented:
Object3 illegal state: Object2 illegal state: Object1 illegal state: value is 0
Be sure to also read blog post: Error handling and Go
There are various libraries that embed stack traces in Go errors. Simply create your error with one of those, and it will bubble up with the full stack context you can later inspect or log.
One such library:
https://github.com/go-errors/errors
And there are a few others I forgot.
Related
Everywhere I look, the "way" to "wrap" errors in Go is to use fmt.Errof with the %w verb
https://go.dev/blog/go1.13-errors
However, fmt.Errorf does not recursively wrap errors. There is no way to use it to wrap three previously defined errors (Err1, Err2, and Err3) and then check the result by using Is() and get true for each those three errors.
FINAL EDIT:
Thanks to #mkopriva's answer and comments below it, I now have a straightforward way to implement this (although, I am still curious if there is some standard type which does this). In the absence of an example, my attempts at creating one failed. The piece I was missing was adding an Is and As method to my type. Because the custom type needs to contain an error and a pointer to the next error, the custom Is and As methods allows us to compare the error contained in the custom type, rather than the custom type itself.
Here is a working example: https://go.dev/play/p/6BYGgIb728k
Highlights from the above link
type errorChain struct {
err error
next *errorChain
}
//These two functions were the missing ingredient
//Defined this way allows for full functionality even if
//The wrapped errors are also chains or other custom types
func (c errorChain) Is(err error) bool { return errors.Is(c.err, err) }
func (c errorChain) As(target any) bool { return errors.As(c.err, target) }
//Omitting Error and Unwrap methods for brevity
func Wrap(errs ...error) error {
out := errorChain{err: errs[0]}
n := &out
for _, err := range errs[1:] {
n.next = &errorChain{err: err}
n = n.next
}
return out
}
var Err0 = errors.New("error 0")
var Err1 = errors.New("error 1")
var Err2 = errors.New("error 2")
var Err3 = errors.New("error 3")
func main() {
//Check basic Is functionality
errs := Wrap(Err1, Err2, Err3)
fmt.Println(errs) //error 1: error 2: error 3
fmt.Println(errors.Is(errs, Err0)) //false
fmt.Println(errors.Is(errs, Err2)) //true
}
While the Go source specifically mentions the ability to define an Is method, the example does not implement it in a way that can solve my issue and the discussion do not make it immediately clear that it would be needed to utilize the recursive nature of errors.Is.
AND NOW BACK TO THE ORIGINAL POST:
Is there something built into Go where this does work?
I played around with making one of my own (several attempts), but ran into undesirable issues. These issues stem from the fact that errors in Go appear to be compared by address. i.e. if Err1 and Err2 point to the same thing, they are the same.
This causes me issues. I can naively get errors.Is and errors.As to work recursively with a custom error type. It is straightforward.
Make a type that implements the error interface (has an Error() string method)
The type must have a member that represents the wrapped error which is a pointer to its own type.
Implement an Unwrap() error method that returns the wrapped error.
Implement some method which wraps one error with another
It seems good. But there is trouble.
Since errors are pointers, if I make something like myWrappedError = Wrap(Err1, Err2) (in this case assume Err1 is being wrapped by Err2). Not only will errors.Is(myWrappedError, Err1) and errors.Is(myWrappedError, Err2) return true, but so will errors.Is(Err2, Err1)
Should the need arise to make myOtherWrappedError = Wrap(Err3, Err2) and later call errors.Is(myWrappedError, Err1) it will now return false! Making myOtherWrappedError changes myWrappedError.
I tried several approaches, but always ran into related issues.
Is this possible? Is there a Go library which does this?
NOTE: I am more interested in the presumably already existing right way to do this rather than the specific thing that is wrong with my basic attempt
Edit 3: As suggested by one of the answers, the issue in my first code is obviously that I modify global errors. I am aware, but failed to adequately communicate. Below, I will include other broken code which uses no pointers and modifies no globals.
Edit 4: slight modification to make it work more, but it is still broken
See https://go.dev/play/p/bSytCysbujX
type errorGroup struct {
err error
wrappedErr error
}
//...implemention Unwrap and Error excluded for brevity
func Wrap(inside error, outside error) error {
return &errorGroup{outside, inside}
}
var Err1 = errorGroup{errors.New("error 1"), nil}
var Err2 = errorGroup{errors.New("error 2"), nil}
var Err3 = errorGroup{errors.New("error 3"), nil}
func main() {
errs := Wrap(Err1, Err2)
errs = Wrap(errs, Err3)
fmt.Println(errs)//error 3: error 2: error 1
fmt.Println(errors.Is(errs, Err1)) //true
fmt.Println(errors.Is(errs, Err2)) //false <--- a bigger problem
fmt.Println(errors.Is(errs, Err3)) //false <--- a bigger problem
}
Edit 2: playground version shortened
See https://go.dev/play/p/swFPajbMcXA for an example of this.
EDIT 1: A trimmed version of my code focusing on the important parts:
type errorGroup struct {
err error
wrappedErr *errorGroup
}
//...implemention Unwrap and Error excluded for brevity
func Wrap(errs ...*errorGroup) (r *errorGroup) {
r = &errorGroup{}
for _, err := range errs {
err.wrappedErr = r
r = err
}
return
}
var Err0 = &errorGroup{errors.New("error 0"), nil}
var Err1 = &errorGroup{errors.New("error 1"), nil}
var Err2 = &errorGroup{errors.New("error 2"), nil}
var Err3 = &errorGroup{errors.New("error 3"), nil}
func main() {
errs := Wrap(Err1, Err2, Err3)//error 3: error 2: error 1
fmt.Println(errors.Is(errs, Err1)) //true
//Creating another wrapped error using the Err1, Err2, or Err3 breaks the previous wrap, errs.
_ = Wrap(Err0, Err2, Err3)
fmt.Println(errors.Is(errs, Err1)) //false <--- the problem
}
You can use something like this:
type errorChain struct {
err error
next *errorChain
}
func Wrap(errs ...error) error {
out := errorChain{err: errs[0]}
n := &out
for _, err := range errs[1:] {
n.next = &errorChain{err: err}
n = n.next
}
return out
}
func (c errorChain) Is(err error) bool {
return c.err == err
}
func (c errorChain) Unwrap() error {
if c.next != nil {
return c.next
}
return nil
}
https://go.dev/play/p/6oUGefSxhvF
Your code modifies package-global error values, so it is inherently broken. This defect has nothing to do with Go's error handling mechanics.
Per the documentation you linked, there are two error-handling helpers: Is, and As. Is lets you recursively unwrap an error, looking for a specific error value, which is necessarily a package global for this to be useful. As, on the other hand, lets you recursively unwrap an error looking for any wrapped error value of a given type.
How does wrapping work? You wrap error A in a new error value B. A Wrap() helper would necessarily return a new value, as fmt.Errorf does in the examples in the linked documentation. A Wrap helper should never modify the value of the error being wrapped. That value should be considered immutable. In fact, in any normal implementation, the value would be of type error, so that you can wrap any error, rather than just wrapping concentric values of your custom error type in each other; and, in that case, you have no access to the fields of the wrapped error to modify them anyway. Essentially, Wrap should be roughly:
func Wrap(err error) error {
return &errGroup{err}
}
And that's it. That's not very useful, because your implementation of errGroup doesn't really do anything - it provides no details about the error that occurred, it's just a container for other errors. For it to have value, it should have a string error message, or methods like some other error types' IsNotFound, or something that makes it more useful than just using error and fmt.Errorf.
Based on the usage in your example code, it also looks like you're presuming the use case is to say "I want to wrap A in B in C", which I've never seen in the wild and I cannot think of any scenario where that would be needed. The purpose of wrapping is to say "I've recieved error A, I'm going to wrap it in error B to add context, and return it". The caller might wrap that error in error C, and so on, which is what makes recursive wrapping valuable.
For example: https://go.dev/play/p/XeoONx19dgX
Instead of chaining/wrapping, you will "soon" (Go 1.20, as seen in Go 1.20-rc1 in Dec. 2022) be able to return a slice/tree of errors.
(In the meantime, mdobak/go-xerrors is a good alternative)
The release note explains:
Wrapping multiple errors
Go 1.20 expands support for error wrapping to permit an error to wrap
multiple other errors.
An error e can wrap more than one error by providing an Unwrap method
that returns a []error.
The errors.Is and errors.As functions have been updated to inspect
multiply wrapped errors.
The fmt.Errorf function now supports multiple occurrences of the %w
format verb, which will cause it to return an error that wraps all of
those error operands.
The new function errors.Join returns an error wrapping a list of
errors.
That comes from:
proposal: errors: add support for wrapping multiple errors
Background
Since Go 1.13, an error may wrap another by providing an Unwrap method returning the wrapped error.
The errors.Is and errors.As functions operate on chains of wrapped errors.
A common request is for a way to combine a list of errors into a single error.
Proposal
An error wraps multiple errors if its type has the method
Unwrap() []error
Reusing the name Unwrap avoids ambiguity with the existing singular Unwrap method.
Returning a 0-length list from Unwrap means the error doesn't wrap anything.
Callers must not modify the list returned by Unwrap.
The list returned by Unwrap must not contain any nil errors.
We replace the term "error chain" with "error tree".
The errors.Is and errors.As functions are updated to unwrap multiple errors.
Is reports a match if any error in the tree matches.
As finds the first matching error in a inorder preorder traversal of the tree.
The errors.Join function provides a simple implementation of a multierr.
It does not flatten errors.
// Join returns an error that wraps the given errors.
// Any nil error values are discarded.
// The error formats as the text of the given errors, separated by newlines.
// Join returns nil if errs contains no non-nil values.
func Join(errs ...error) error
The fmt.Errorf function permits multiple instances of the %w formatting verb.
The errors.Unwrap function is unaffected: It returns nil when called on an error with an Unwrap() []error method.
Why should this be in the standard library?
This proposal adds something which cannot be provided outside the standard library: Direct support for error trees in errors.Is and errors.As.
Existing combining errors operate by providing Is and As methods which inspect the contained errors, requiring each implementation to duplicate this logic, possibly in incompatible ways.
This is best handled in errors.Is and errors.As, for the same reason those functions handle singular unwrapping.
In addition, this proposal provides a common method for the ecosystem to use to represent combined errors, permitting interoperation between third-party implementations.
So far (Sept. 2022) this proposal seems a likely accept has been accepted!
CL 432575 starts the implementation.
There arr several approaches but there is one thing that you should keep in mind: if you have multiple errors, you may need to handle it as a slice of errors
For instance, imagine you need to check if all errors are the same, or there is at least one error of certain type you can use the snippet below.
You can extend this concept or use some existing library to handle multierrors
type Errors []error
func (errs Errors) String() string {
…
}
func (errs Errors) Any(target error) bool{
for _, err := range errs {
if errors.Is(err,target) {
return true
}
}
return false
}
func (errs Errors) All(target error) bool{
if len(errs) == 0 { return false }
for _, err := range errs {
if !errors.Is(err,target) {
return false
}
}
return true
}
One of the main issues I have with Golang is that the error handling is basically a check for a string (I would love to be wrong, do not hesitate :))
In the example below, I am trying to create a directory, but will have different behaviour depending on the kind of issue. Specifically, if a directory exists I will just pass.
package main
import (
"fmt"
"os"
)
func main() {
err := os.Mkdir("test", 0644)
if err != nil {
fmt.Printf("error: %v", err)
if err.Error() == "mkdir test: Cannot create a file when that file already exists" {
fmt.Printf("the dir already exists")
} else {
panic(err)
}
}
}
It does not work, repeated attempts are not logged. Why? Ah, crap, I forgot the dot at the end of the mkdir test: Cannot create a file when that file already exists string.
I feel that relying on an error string is fragile, as opposed to having something like err.ErrorType().DirectoryExists() kind of check (which sometimes exists, in net for instance).
My question: to what extent can I rely on the fact that the error strings will not change? (in other words, that mkdir test: Cannot create a file when that file already exists. will not be worded differently, or ported to another national language, etc.)
I had some hope with errors.Is() but it ultimately relies on the string comparison.
Go error strings don't change arbitrarily, but they also aren't covered by the Go compatibility policy: they can be changed if the increase in clarity outweighs the (inevitable) cost of breaking programs that make (fragile, unsupported) assumptions about the string contents.
The errors package is the robust way to check for specific types of errors.
Use errors.Is to check for equivalence to a canonical error (https://play.golang.org/p/co6ukgQrr58):
err := os.Mkdir(dir, 0644)
if errors.Is(err, os.ErrExist) {
t.Logf("the dir already exists")
} else if err != nil {
t.Fatal(err)
}
Use errors.As to check for a particular type of error (https://play.golang.org/p/UR1nUCRMUY6):
err := os.Mkdir(dir, 0644)
var pe *os.PathError
if errors.As(err, &pe) {
t.Logf("error creating %q: %v", pe.Path, pe.Err)
} else if err != nil {
t.Fatal(err)
}
In this case, you can use os.IsExist(err)
err := os.Mkdir("test", 0644)
if err != nil {
if os.IsExist(err){
fmt.Printf("the dir already exists")
} else {
panic(err)
}
}
Good libraries should allow you to inspect errors without relying on string comparison. Various methods exist to do so:
Comparaison with sentinel values if err == os.EOF
Utility function: os.IsExist(err)
Type assertion: pathErr := err.(*os.PathError)
There is always a way to inspect errors in the standard library without relying on strings. Check the function/package documentation for details about how to do it.
Note:
errors.Is() and errors.As() are a (~recent) generalisation of == and type assertion but for errors that could contain other errors. See https://go.dev/blog/go1.13-errors
From https://pkg.go.dev/os#Mkdir:
Mkdir creates a new directory with the specified name and permission bits (before umask). If there is an error, it will be of type *PathError.
This means you could type-assert the returned error to get more information.
if err != nil {
pathErr := err.(*os.PathError)
}
With errors returned from functions in package os specifically, also take note of these two functions:
https://pkg.go.dev/os#IsExist
https://pkg.go.dev/os#IsNotExist
to what extent can I rely on the fact that the error strings will not change?
To the extent which is guaranteed by the function's contract, which as in most programming languages conventionally is written in documenting comments above the function. In the case of os.MkDir(): you cannot.
I'm pretty new to golang so sorry for my question.
I have the following function:
func (app *application) serverError(w http.ResponseWriter, err error) {
trace := fmt.Sprintf("%s\n%s", err.Error(), debug.Stack())
app.errorLog.Output(2, trace)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
I'm using golangci-lint linter, and when I launch the linter, it returns the following error:
cmd/web/helpers.go:15:22: Error return value of `app.errorLog.Output` is not checked (errcheck)
app.errorLog.Output(2, trace)
^
How can I fix that?
In go, a common pattern is to have a function that returns two values, the first of which is the desired result, and the second of which is type error.
Typically, if an implementation cannot provide a value because of some error e, it will return nil, e or return <zero value for type>, e.
It doesn't just have to be one such desired result value, however - sometimes there will be zero or more than one desired result values. Basic rule: if the last return value of a function is error typed, and the docs don't say otherwise, always check that last return value.
So, when you see such a pattern in a return signature - you should not discard the last result with a ignored return or an assign to '_', but should check that value to make sure it is non-nil before continuing.
The first of those anti-patterns is what the linter is warning you about. You can check the error argument thusly (I'm assuming that there are zero "desired result" values here):
if err := app.errorLog.Output(2, trace); err != nil {
// ... do something to handle error here or panic(err)
}
This will satisfy the linter and make your code more robust!
I would like to know how can I check the type of error returned by plugin.Open, e.g:
package main
import "plugin"
func main() {
_, err := plugin.Open("./module.so")
// here
}
I would like to do something different if the error is:
plugin.Open("./module.so"): realpath failed
Which basically means that the file doesn't exist.
Example of desired result:
package main
import "plugin"
func main() {
_, err := plugin.Open("./module.so")
if err.Error() == "plugin.Open(\"./module.so\"): realpath failed" {
// do something different here
} else {
log.Fatal(err)
}
}
The string that I pass to plugin.Open can have other values, so it needs to be something more smart than that.
Thanks in advance.
Inspection of the code for plugin.Open() reveals the package calls out to some C code to determine whether the path exists. If it doesn't, it returns a plain error value. In particular, the package does not define any sentinel errors which you can compare against, nor does it return its own concrete implementer of the error interface which carries custom metadata. This is the code which produces that error:
return nil, errors.New(`plugin.Open("` + name + `"): realpath failed`)
errors.New is a basic implementation of the error interface which doesn't allow any additional information to be passed. Unlike other locations in the standard library which return errors (such as path non-existent errors from the os package), you can't get such metadata in this instance.
Check whether the module file exists first
My preference would be to verify whether the module exists before attempting to load it, using the native capabilities provided by the os package:
modulePath := "./module.so"
if _, err := os.Stat(modulePath); os.IsNotExist(err) {
// Do whatever is required on module not existing
}
// Continue to load the module – can be another branch of the if block
// above if necessary, depending on your desired control flow.
Compare a subset of the error values
You could also use strings.Contains to search for the value realpath failed in the returned error value. This is not a good idea in the event that string changes in future, so if you adopt this pattern, at the very least you should ensure you have rigorous tests around it (and even then it's still not great).
_, err := plugin.Open("./module.so")
if err != nil {
if strings.Contains(err.Error(), "realpath failed") {
// Do your fallback behavior for module not existing
log.Fatalf("module doesn't exist")
} else {
// Some other type of error
log.Fatalf("%+v", err)
}
}
I wondering what is the best way to handle error form multiple level abstraction in go. Every time if I must add a new level abstraction to program, I am forced to transfer error code from level less to level high. Thereby is duplicate communitaces in log file or I must remmember to delete communicate form level low and transfer him to level higher. Below simply example. I skipped creating each object to more shortly and celar code, but I think You understand my problem
type ObjectOne struct{
someValue int
}
func (o* ObjectOne)CheckValue()error{
if o.someValue == 0 {
SomeLogger.Printf("Value is 0 error program") // communicate form first level abstraction to logger
return errors.New("Internal value in object is 0")
}
return nil
}
type ObjectTwoHigherLevel struct{
objectOne ObjectOne
}
func (oT* ObjectTwoHigherLevel)CheckObjectOneIsReady() error{
if err := oT.objectOne.CheckValue() ; err != nil{
SomeLogger.Printf("Value in objectOne is not correct for objectTwo %s" , err) // second communicate
return err
}
return nil
}
type ObjectThreeHiggerLevel struct{
oT ObjectTwoHigherLevel
}
func (oTh* ObjectThreeHiggerLevel)CheckObjectTwoIsReady()error{
if err := oTh.oT.CheckObjectOneIsReady() ; err != nil{
SomeLogger.Printf("Value in objectTwo is not correct for objectThree %s" , err)
return err
}
return nil
}
In result in log file I get duplicate posts
Value is 0 error program
Value in objectOne is not correct for objectTwo Internal value in object is 0
Value in objectTwo is not correct for objectThree Internal value in object is 0
In turn if I only transfer some err to higher level without additional log I lost information what happend in each level.
How this solve ? How privent duplicate communicates ? Or My way is the good and the only ?
Problem is more frustrating if I create a few object which search something in database on a few abstraction level then I get also few lines form this same task in logFile.
EDIT: This answer pre-dates Go 1.13 which provides something similar to the presented technique. Please check The Go Blog: Working with Errors in Go 1.13.
You should either handle an error, or not handle it but delegate it to a higher level (to the caller). Handling the error and returning it is bad practice as if the caller also does the same, the error might get handled several times.
Handling an error means inspecting it and making a decision based on that, which may be you simply log it, but that also counts as "handling" it.
If you choose to not handle but delegate it to a higher level, that may be perfectly fine, but don't just return the error value you got, as it may be meaningless to the caller without context.
Annotating errors
A really nice and recommended way of delegation is Annotating errors. This means you create and return a new error value, but the old one is also wrapped in the returned value. The wrapper provides the context for the wrapped error.
There is a public library for annotating errors: github.com/pkg/errors; and its godoc: errors
It basically has 2 functions: 1 for wrapping an existing error:
func Wrap(cause error, message string) error
And one for extracting a wrapped error:
func Cause(err error) error
Using these, this is how your error handling may look like:
func (o *ObjectOne) CheckValue() error {
if o.someValue == 0 {
return errors.New("Object1 illegal state: value is 0")
}
return nil
}
And the second level:
func (oT *ObjectTwoHigherLevel) CheckObjectOneIsReady() error {
if err := oT.objectOne.CheckValue(); err != nil {
return errors.Wrap(err, "Object2 illegal state: Object1 is invalid")
}
return nil
}
And the third level: call only the 2nd level check:
func (oTh *ObjectThreeHiggerLevel) CheckObjectTwoIsReady() error {
if err := oTh.ObjectTwoHigherLevel.CheckObjectOneIsReady(); err != nil {
return errors.Wrap(err, "Object3 illegal state: Object2 is invalid")
}
return nil
}
Note that since the CheckXX() methods do not handle the errors, they don't log anything. They are delegating annotated errors.
If someone using ObjectThreeHiggerLevel decides to handle the error:
o3 := &ObjectThreeHiggerLevel{}
if err := o3.CheckObjectTwoIsReady(); err != nil {
fmt.Println(err)
}
The following nice output will be presented:
Object3 illegal state: Object2 is invalid: Object2 illegal state: Object1 is invalid: Object1 illegal state: value is 0
There is no pollution of multiple logs, and all the details and context are preserved because we used errors.Wrap() which produces an error value which formats to a string which preserves the wrapped errors, recursively: the error stack.
You can read more about this technique in blog post:
Dave Cheney: Don’t just check errors, handle them gracefully
"Extending" errors
If you like things simpler and / or you don't want to hassle with external libraries and you're fine with not being able to extract the original error (the exact error value, not the error string which you can), then you may simply extend the error with the context and return this new, extended error.
Extending an error is easiest done by using fmt.Errorf() which allows you to create a "nice" formatted error message, and it returns you a value of type error so you can directly return that.
Using fmt.Errorf(), this is how your error handling may look like:
func (o *ObjectOne) CheckValue() error {
if o.someValue == 0 {
return fmt.Errorf("Object1 illegal state: value is %d", o.someValue)
}
return nil
}
And the second level:
func (oT *ObjectTwoHigherLevel) CheckObjectOneIsReady() error {
if err := oT.objectOne.CheckValue(); err != nil {
return fmt.Errorf("Object2 illegal state: %v", err)
}
return nil
}
And the third level: call only the 2nd level check:
func (oTh *ObjectThreeHiggerLevel) CheckObjectTwoIsReady() error {
if err := oTh.ObjectTwoHigherLevel.CheckObjectOneIsReady(); err != nil {
return fmt.Errorf("Object3 illegal state: %v", err)
}
return nil
}
And the following error message would be presented at ObjectThreeHiggerLevel should it decide to "handle" it:
o3 := &ObjectThreeHiggerLevel{}
if err := o3.CheckObjectTwoIsReady(); err != nil {
fmt.Println(err)
}
The following nice output will be presented:
Object3 illegal state: Object2 illegal state: Object1 illegal state: value is 0
Be sure to also read blog post: Error handling and Go
There are various libraries that embed stack traces in Go errors. Simply create your error with one of those, and it will bubble up with the full stack context you can later inspect or log.
One such library:
https://github.com/go-errors/errors
And there are a few others I forgot.