Mastering Go 1.13 Error Handling: Unwrap, Is, As, and %w
Go 1.13 introduced new error‑handling utilities—Unwrap methods, errors.Is and errors.As functions, and the %w formatting verb—allowing developers to wrap, inspect, and propagate underlying errors more effectively, with examples illustrating legacy patterns, custom error types, and best‑practice decisions on when to wrap.
Introduction
Early Go treated errors as ordinary values, and the standard library only provided errors.New and fmt.Errorf, which produced errors containing a single message. The built‑in error interface lets developers embed any additional information by defining a type that implements the Error() method.
type QueryError struct {
Query string
Err error
}
func (e *QueryError) Error() string { return e.Query + ": " + e.Err.Error() }Such custom error types are ubiquitous; they may carry timestamps, file names, server addresses, or a lower‑level error for extra context.
Pre‑Go 1.13 Error Handling
Checking Errors
Typical error checks compare the error value to nil or to a sentinel value.
if err != nil {
// handle error
}
var ErrNotFound = errors.New("not found")
if err == ErrNotFound {
// not found handling
}Errors can also be type‑asserted or type‑switched to a more specific struct.
type NotFoundError struct { Name string }
func (e *NotFoundError) Error() string { return e.Name + ": not found" }
if e, ok := err.(*NotFoundError); ok {
// e.Name is the missing item
}Adding Context
When propagating an error up the call stack, developers often add a short description.
if err != nil {
return fmt.Errorf("decompress %v: %v", name, err)
}Creating a new error type that embeds the underlying error (e.g., QueryError) enables “unpacking” the error later.
if e, ok := err.(*QueryError); ok && e.Err == ErrPermission {
// query failed due to permission issue
}Go 1.13 and Later Error Handling
Unwrap Method
Go 1.13 adds a convention: an error that wraps another implements an Unwrap() error method returning the inner error. This creates an error chain.
func (e *QueryError) Unwrap() error { return e.Err }Using errors.Is and errors.As
The new errors.Is function checks whether any error in the chain matches a target value (similar to sentinel comparison). errors.As checks whether any error in the chain can be assigned to a target type.
if errors.Is(err, ErrNotFound) {
// not found handling
}
var e *QueryError
if errors.As(err, &e) {
// err is a *QueryError, e holds the concrete value
}Both functions traverse the entire chain, so they work with wrapped errors as well.
Formatting with %w
fmt.Errorfnow supports the %w verb. When used, the returned error implements Unwrap and returns the wrapped error.
if err != nil {
return fmt.Errorf("decompress %v: %w", name, err)
}
err := fmt.Errorf("access denied: %w", ErrPermission)
if errors.Is(err, ErrPermission) {
// permission error detected
}When to Wrap Errors
Whether to wrap depends on the API contract. If exposing the underlying error is part of the public contract, wrap with %w. If the underlying error is an implementation detail, either omit it or wrap with %v to hide the inner type.
// Example: exposing sql.ErrNoRows deliberately
err := pkg.LookupUser(...)
if errors.Is(err, sql.ErrNoRows) { /* handle missing row */ }
// Example: hiding internal *os.PathError
if err != nil {
return fmt.Errorf("%v", err) // %v hides the concrete type
}Custom Is Method
Types can implement an Is(error) bool method to define custom matching logic, as shown by the Upspin‑inspired example.
type Error struct { Path, User string }
func (e *Error) Is(target error) bool {
t, ok := target.(*Error)
if !ok { return false }
return (e.Path == t.Path || t.Path == "") &&
(e.User == t.User || t.User == "")
}
if errors.Is(err, &Error{User: "someuser"}) {
// err's User field matches "someuser"
}Errors and Package APIs
A well‑designed package should document which errors callers can rely on and avoid leaking internal details. The simplest contract is “nil on success, non‑nil on failure.” When a specific condition must be signaled, return a wrapped sentinel error.
var ErrNotFound = errors.New("not found")
func FetchItem(name string) (*Item, error) {
if itemNotFound(name) {
return nil, fmt.Errorf("%q: %w", name, ErrNotFound)
}
// ...
}When returning errors from another package, decide whether the underlying error should be part of the public API. If not, wrap it with %v or a custom error type that hides the implementation detail.
if err != nil {
return fmt.Errorf("%v", err) // hides *os.PathError
}Conclusion
Go 1.13’s three new functions ( errors.Is, errors.As, errors.Unwrap) and the %w formatting verb dramatically improve error handling by making error chains explicit, encouraging consistent wrapping for context, and giving callers reliable tools to inspect and react to errors.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Ops Development & AI Practice
DevSecOps engineer sharing experiences and insights on AI, Web3, and Claude code development. Aims to help solve technical challenges, improve development efficiency, and grow through community interaction. Feel free to comment and discuss.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
