Go Error Wrapping: Difference between revisions

From NovaOrdis Knowledge Base
Jump to navigation Jump to search
 
(49 intermediate revisions by the same user not shown)
Line 1: Line 1:
=External=
* https://pkg.go.dev/errors
=Internal=
=Internal=
* [[Go_Language_Error_Handling#Wrap_the_Error_Instance|Go Error Handling]]
* [[Go_Language_Error_Handling#Wrap_the_Error_Instance|Go Error Handling]]


=Overview=
=Overview=
Go error mechanism 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.
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 [[Go_Language_Error_Handling#Deal_with_the_Consequences_and_Continue_the_Flow|fully handling the error without returning it]], simply [[Go_Language_Error_Handling#Handle_the_Error_as_Opaque_and_Return_It_Unmodified|returning it without any modification]], and [[Go_Language_Error_Handling#Annotate_the_Error|returning a new annotated error]].
Error wrapping should be the preferred pattern to use when we want to provide additional context to an 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 [[Go_Language_Error_Handling#Annotate_the_Error|error annotation]], because it involves embedding an actual error instance, preserving its unique identity, instead of concatenating strings.
Error wrapping and returning the result to the upper layer is just one of the error handling patterns in Go. The others are [[Go_Language_Error_Handling#Deal_with_the_Consequences_and_Continue_the_Flow|fully handling the error without returning it]], simply [[Go_Language_Error_Handling#Handle_the_Error_as_Opaque_and_Return_It_Unmodified|returning it without any modification]], and [[Go_Language_Error_Handling#Annotate_the_Error|returning a new annotated error]].
 
This 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. For example, if access to a database fails with access denied, handing the error may consist in wrapping the original error into a new error that also includes the user and the operation, which is richer context that can help with troubleshooting the problem by the operators of the program.
 
<span id='Mark'></span>Another situation when wrapping the error is useful is when we want to '''mark the error as a specific type''' by wrapping into a [[Go_Custom_Error_Types#Mark|custom error type]]. For example, the same access denied to the database can be wrapped into a "Forbidden" error type, so a hypothetical HTTP handling layer that receives the error returns a 403 status code.
 
These approaches can be combined: we can at the same time add more context and mark an error.
 
In all these cases, the source error remains available for further inspection. This is different from and more than [[Go_Language_Error_Handling#Annotate_the_Error|annotating]] the error, because it involves embedding an actual error instance, preserving its unique identity, instead of concatenating strings. The wrapped error can be accessed with the <code>[[#errors.Unwrap.28.29|errors.Unwrap()]]</code> function, and the error tree can be navigated recursively this way.
 
One disadvantage of error wrapping is that introduces potential coupling. If a calling layer checks for a specific wrapped error, and the called layer changes its implementation and returns a different wrapped error, this will break the calling layer. If this a problem, [[Go_Language_Error_Handling#Annotate_the_Error|annotating]] the errors instead of wrapping them is a better approach.


=Wrap the Error=
=Wrap the Error=
Line 17: Line 29:
<syntaxhighlight lang='go'>
<syntaxhighlight lang='go'>
if err != nil {
if err != nil {
   outer := fmt.Errorf("additional context: %w", err)
   outer := fmt.Errorf("%w: additional context", err)
   return outer
   return outer
}
}
Line 23: Line 35:
<code>fmt.Errorf()</code> call creates a new error instance whose <code>Error()</code> method concatenates the error message provided as argument and the inner error message:
<code>fmt.Errorf()</code> call creates a new error instance whose <code>Error()</code> method concatenates the error message provided as argument and the inner error message:
<font size=-1>
<font size=-1>
  additional context: original context
  original context: additional context
</font>
</font>
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 <code>errors.Is()</code> function:
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 <code>errors.Is()</code> function:
<syntaxhighlight lang='go'>
<syntaxhighlight lang='go'>
errors.Is(outer, err) // returns true
errors.Is(outer, err) // returns true
</syntaxhighlight>
</syntaxhighlight>
The outer error becomes the direct ancestor of the wrapped error, and successive invocations of the <code>fmt.Errorf()</code> result in an error tree, where the identity of the wrapped errors can be checked with  <code>errors.Is()</code>. More details on <code>errors.Is()</code> can be found [[#errors.Is.28.29|below]]. The wrapped error can be obtained from the outer error by calling <code>[[#errors.Unwrap.28.29|errors.Unwrap()]]</code>.
The outer error becomes the direct ancestor of the wrapped error, and successive invocations of the <code>fmt.Errorf()</code> result in an error tree, where the identity of the wrapped errors can be checked with  <code>errors.Is()</code>. More details on <code>errors.Is()</code> can be found [[#errors.Is.28.29|below]]. The wrapped error can be obtained from the outer error by calling <code>[[#errors.Unwrap.28.29|errors.Unwrap()]]</code>.
===Invoking <tt>fmt.Errorf()</tt> with More that One Error Argument===
<code>fmt.Errorf()</code> is conventionally invoked with just one "%w" conversion character and one error argument. This results in building a [[Tree_Concepts#Degenerated_.28Pathological.29_Tree|degenerated]] tree that is in fact a linked list. However, more than one error can be provided as argument to <code>fmt.Errorf()</code>:
<syntaxhighlight lang='go'>
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
</syntaxhighlight>
The behavior of <code>errors.Unwrap()</code> seems to be a bit [[#Unwrap_Inconsistency|inconsistent]] in this situation: it won't return the error instances, but <code>nil</code>. This seems to imply that <code>fmt.Errors()</code> should only be used to wrap a single error.
==Wrap a Lower Error with a Sentinel Error==
This pattern is useful when we want to add "typing" to a lower error, not just annotate it to a message.
<syntaxhighlight lang='go'>
var ErrServiceFailure = errors.New("service failure")
func processServiceResponse() (string, err) {
    result, err := service.invoke()
    if err != nil {
        // we bubble up the underlying service failure, but we
        // annotate it with a "typed" ErrServiceFailure, which can
        // then be identified with errors.Is()
        return "", fmt.Errorf("%w: %w", ErrServiceFailure, err)
    }
    // process response
    // return response-related errors
    ...
    return "", fmt.Errorf("unexpected result: %s", result)
}
</syntaxhighlight>


==<tt>errors.Join()</tt>==
==<tt>errors.Join()</tt>==
Line 38: Line 84:
==<tt>errors.Is()</tt>==
==<tt>errors.Is()</tt>==
{{External|https://pkg.go.dev/errors#Is}}
{{External|https://pkg.go.dev/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 <code>errors.Is(<outer_error>, <sought_for_error>)</code> function:
The presence of a '''specific error instance''' wrapped inside an outer error, as a direct or indirect descendant in the error tree rooted in the outer error, can be confirmed with <code>errors.Is(<outer_error>, <sought_for_error>)</code> function. The function introspects the <code>outer_error</code>'s error tree by repeatedly calling <code>Unwrap()</code>.


Always prefer <code>errors.Is()</code> over the <code>==</code> equality operator. <code>errors.Is()</code> is designed to work with both individual errors and wrapped errors. Furthermore, <code>errors.Is()</code> is safe to use if the first argument, <code>outer_error</code> is <code>nil</code>.
<syntaxhighlight lang='go'>
<syntaxhighlight lang='go'>
orig := fmt.Errorf("original error")
orig := fmt.Errorf("original error")
Line 50: Line 97:


==<tt>errors.As()</tt>==
==<tt>errors.As()</tt>==
{{External|https://pkg.go.dev/errors#As}}
<code>errors.As(<outer_error>, <an_error_similar_to_the_error_sought_after>)</code> provides a safe way to locate the first error in the error tree that matches the '''error type''' of the error instance provided as the second argument. If no match is found, the function returns false.  The function introspects the <code>outer_error</code>'s error tree by repeatedly calling <code>Unwrap()</code>. <code>errors.As()</code> is similar, but weaker form of <code>errors.Is()</code> with the difference that types, and not identities are compared. Always prefer <code>errors.As()</code> over type assertions.
==<tt>errors.Unwrap()</tt>==
==<tt>errors.Unwrap()</tt>==
{{External|https://pkg.go.dev/errors#Unwrap}}
<code>errors.Unwrap(<outer_error>)</code> returns the inner error instance wrapped inside the outer error by a <code>fmt.Errorf("%w")</code> call. <code>errors.Unwrap()</code> does not unwrap errors returned by <code>[[#errors.Join.28.29|errors.Join()]]</code>.
<syntaxhighlight lang='go'>
<syntaxhighlight lang='go'>
orig := fmt.Errorf("original error")
orig := fmt.Errorf("original error")
Line 58: Line 110:
println(errors.Unwrap(intermediate) == orig)  // prints true
println(errors.Unwrap(intermediate) == orig)  // prints true
</syntaxhighlight>
</syntaxhighlight>
<span id='Unwrap_Inconsistency'></span>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>


=TO DEPLETE=
===<tt>Unwrap() errror</tt> Implementation===
 
Example: {{Internal|Go_Custom_Error_Types#Unwrap_Implementation|Custom Error Types &#124; <tt>Unwrap()</tt> implementation}}
====Checking for Wrapped Errors====
 
 
Need to understand errors.Is(), error tree, wrapping and unwrapping.
 
<syntaxhighlight lang='go'>
var BlueError = errors.New("some information")
var GreenError = errors.New("some information")
 
...
 
// wrap the error in an outer error
outerError := fmt.Errorf("addtional info: %w", BlueError)
 
if errors.Is(outerError, BlueError) {
  fmt.Println("found blue error")
}
 
if errors.Is(outerError, GreenError) {
  fmt.Println("found green error")
}
</syntaxhighlight>
 
<code>BlueError</code> is correctly identified, even though both <code>BlueError</code> and <code>GreenError</code> carry the same string. <font color=darkkhaki>How?
</font>
</font>

Latest revision as of 23:38, 6 September 2024

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 should be the preferred pattern to use when we want to provide additional context to an error

Error wrapping and returning the result to the upper layer is just one of the 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.

This 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. For example, if access to a database fails with access denied, handing the error may consist in wrapping the original error into a new error that also includes the user and the operation, which is richer context that can help with troubleshooting the problem by the operators of the program.

Another situation when wrapping the error is useful is when we want to mark the error as a specific type by wrapping into a custom error type. For example, the same access denied to the database can be wrapped into a "Forbidden" error type, so a hypothetical HTTP handling layer that receives the error returns a 403 status code.

These approaches can be combined: we can at the same time add more context and mark an error.

In all these cases, the source error remains available for further inspection. This is different from and more than annotating the error, because it involves embedding an actual error instance, preserving its unique identity, instead of concatenating strings. The wrapped error can be accessed with the errors.Unwrap() function, and the error tree can be navigated recursively this way.

One disadvantage of error wrapping is that introduces potential coupling. If a calling layer checks for a specific wrapped error, and the called layer changes its implementation and returns a different wrapped error, this will break the calling layer. If this a problem, annotating the errors instead of wrapping them is a better approach.

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("%w: additional context", 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:

original context: additional 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

The behavior of errors.Unwrap() seems to be a bit inconsistent in this situation: it won't return the error instances, but nil. This seems to imply that fmt.Errors() should only be used to wrap a single error.

Wrap a Lower Error with a Sentinel Error

This pattern is useful when we want to add "typing" to a lower error, not just annotate it to a message.


var ErrServiceFailure = errors.New("service failure")


func processServiceResponse() (string, err) {
    result, err := service.invoke()
    if err != nil {
        // we bubble up the underlying service failure, but we 
        // annotate it with a "typed" ErrServiceFailure, which can 
        // then be identified with errors.Is()
        return "", fmt.Errorf("%w: %w", ErrServiceFailure, err)
    }
    // process response
    // return response-related errors
    ...
    return "", fmt.Errorf("unexpected result: %s", result) 
}

errors.Join()

https://pkg.go.dev/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()

https://pkg.go.dev/errors#Is

The presence of a specific error instance wrapped 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. The function introspects the outer_error's error tree by repeatedly calling Unwrap().

Always prefer errors.Is() over the == equality operator. errors.Is() is designed to work with both individual errors and wrapped errors. Furthermore, errors.Is() is safe to use if the first argument, outer_error is nil.

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()

https://pkg.go.dev/errors#As

errors.As(<outer_error>, <an_error_similar_to_the_error_sought_after>) provides a safe way to locate the first error in the error tree that matches the error type of the error instance provided as the second argument. If no match is found, the function returns false. The function introspects the outer_error's error tree by repeatedly calling Unwrap(). errors.As() is similar, but weaker form of errors.Is() with the difference that types, and not identities are compared. Always prefer errors.As() over type assertions.

errors.Unwrap()

https://pkg.go.dev/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.

Unwrap() errror Implementation

Example:

Custom Error Types | Unwrap() implementation