Go Error Wrapping: Difference between revisions
Line 71: | Line 71: | ||
println(errors.Unwrap(intermediate) == orig) // prints true | println(errors.Unwrap(intermediate) == orig) // prints true | ||
</syntaxhighlight> | </syntaxhighlight> | ||
When more than one errors is wrapped with <code>fmt.Errorf()</code>, <code>errors.Unwrap()</code> returns <code>nil</code>. <font color=darkkhaki>This seems to be a bit inconsistent, and implies that <code>fmt.Errorf()</code> should only be used to wrap single errors.</font> | When [[#Invoking_fmt.Errorf.28.29_with_More_that_One_Error_Argument|more than one errors]] is wrapped with <code>fmt.Errorf()</code>, <code>errors.Unwrap()</code> returns <code>nil</code>. <font color=darkkhaki>This seems to be a bit inconsistent, and implies that <code>fmt.Errorf()</code> should only be used to wrap single errors.</font> |
Revision as of 21:18, 28 December 2023
External
Internal
Overview
The error mechanism in Go allows "wrapping" error instances into other error instances, while preserving the wrapped error identity. This pattern supports building an error tree that is useful in preserving context.
Error wrapping and returning the result to the upper layer is one of the common error handling patterns in Go. The others are fully handling the error without returning it, simply returning it without any modification, and returning a new annotated error. The pattern consists in wrapping an error returned by the underlying layer into an "outer" error instance. This is typically done to add information relevant to the context that caught the error being processed. However, wrapping the error is more than error annotation, because it involves embedding an actual error instance, preserving its unique identity, instead of concatenating strings.
Wrap the Error
Individual errors are typically wrapped with fmt.Errorf()
. To wrap multiple error instances into a single outer error, use errors.Join()
.
Wrap an Individual Error
A typical error handling pattern is to handle an error returned by a function invocation by "wrapping" it into an outer error instance that provides additional context, in form of an addition error text message.The approach is different from annotating the error because it creates a new error instance while preserving the identity of the wrapped error. The wrapped error is still reachable by introspecting the error tree with methods like errors.Is()
or errors.As()
.
The error is wrapped with fmt.Errorf()
and %w
conversion character:
if err != nil {
outer := fmt.Errorf("additional context: %w", err)
return outer
}
fmt.Errorf()
call creates a new error instance whose Error()
method concatenates the error message provided as argument and the inner error message:
additional context: original context
The outer error assumes the logical identity of the inner error: the outer error "is" all its inner errors. This can be checked programmatically with errors.Is()
function:
errors.Is(outer, err) // returns true
The outer error becomes the direct ancestor of the wrapped error, and successive invocations of the fmt.Errorf()
result in an error tree, where the identity of the wrapped errors can be checked with errors.Is()
. More details on errors.Is()
can be found below. The wrapped error can be obtained from the outer error by calling errors.Unwrap()
.
Invoking fmt.Errorf() with More that One Error Argument
fmt.Errorf()
is conventionally invoked with just one "%w" conversion character and one error argument. This results in building a degenerated tree that is in fact a linked list. However, more than one error can be provided as argument to fmt.Errorf()
:
err := fmt.Errorf("err")
err2 := fmt.Errorf("err2")
outer := fmt.Errorf("outer: %w %w", err, err2)
println(errors.Is(outer, err)) // prints true
println(errors.Is(outer, err2)) // prints true
errors.Join()
Join returns an error that wraps the given errors. Any nil
error values are discarded. Join returns nil
if every value provided as argument is nil
. The error formats as the concatenation of the strings obtained by calling the Error()
method of each element, with a newline between each string.
Inspect the Error Tree
errors.Is()
The presence of a wrapped error inside an outer error, as a direct or indirect descendant in the error tree rooted in the outer error, can be confirmed with errors.Is(<outer_error>, <sought_for_error>)
function:
orig := fmt.Errorf("original error")
intermediate := fmt.Errorf("intermediate error: %w", orig)
outer := fmt.Errorf("final intermediate error: %w", intermediate)
println(errors.Is(outer, intermediate)) // prints "true"
println(errors.Is(outer, orig)) // prints "true"
println(errors.Is(intermediate, orig)) // prints "true"
errors.As()
errors.Unwrap()
errors.Unwrap(<outer_error>)
returns the inner error instance wrapped inside the outer error by a fmt.Errorf("%w")
call. errors.Unwrap()
does not unwrap errors returned by errors.Join()
.
orig := fmt.Errorf("original error")
intermediate := fmt.Errorf("intermediate error: %w", orig)
outer := fmt.Errorf("final intermediate error: %w", intermediate)
println(errors.Unwrap(outer) == intermediate) // prints true
println(errors.Unwrap(intermediate) == orig) // prints true
When more than one errors is wrapped with fmt.Errorf()
, errors.Unwrap()
returns nil
. This seems to be a bit inconsistent, and implies that fmt.Errorf()
should only be used to wrap single errors.