September 3, 2019·6 min read·engineeringgoerrorsmodulesreleases

Go 1.13 preserves error identity through contextual wrapping

Go 1.13 adds inspectable error wrapping and makes public module authentication a default. Both changes preserve information that callers previously had to reconstruct or trust.

Adding context to an error used to leave a common choice between a useful message and a value the caller could inspect. Go 1.13 gives the standard library a convention for keeping both. An error can expose its underlying cause through Unwrap, and callers can search that chain without parsing the text added by intermediate functions.

Andrew Bonventre's September 3 release announcement names error wrapping among the release's main changes. The release notes also make the public module proxy and checksum database defaults, enable TLS 1.3 by default and introduce language changes to number literals and shift counts. Each deserves its own explanation, but error wrapping has an immediate effect on the contracts between ordinary Go functions.

I like Go's treatment of errors as values because it leaves policy visible to the caller. A library reports what happened, and the caller decides what it means for the current operation. Wrapping improves that arrangement when it preserves enough information for the caller to make a decision after several functions have added context.

Formatting and wrapping express different contracts

The new %w verb in fmt.Errorf takes an error operand. The returned error has an Unwrap method exposing that operand. A message can therefore name the operation while retaining the original error as a value.

For example, this fragment uses the new convention around a file operation. It assumes the ordinary fmt and os imports.

func openConfig(path string) (*os.File, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, fmt.Errorf("open config %q: %w", path, err)
    }
    return f, nil
}

The caller owns the returned file on success and must close it when finished. On failure, the important change is the %w operand. Using %v would include the error's formatted text without creating this particular wrapping relationship.

That relationship is observable by callers. Exposing an underlying error allows them to depend on its identity or type. A wrapper is consequently more than punctuation in a log message. It changes the information offered by the function, and that information can influence retry behavior or classification higher in the call chain.

The release notes define wrapping through Unwrap() error, so a custom error type can implement the convention directly. The outer error remains available with its context, while its underlying value remains available for inspection. No special syntax is required in the language to create that type.

Searching by identity and by type

The new errors.Is searches an error chain for a match with a target. The Go 1.13 implementation starts with the outer error and repeatedly unwraps it. Where the target is comparable, equality can establish a match. An error's own Is(error) bool method can also define a match.

This makes a sentinel comparison useful across contextual wrappers. Direct equality with the outer error would not find the original sentinel merely because its text appears inside a message. Searching the chain follows the values the wrapper deliberately exposes.

errors.As addresses a different question. It finds the first error in the chain assignable to the type represented by its target, or one whose custom As method supplies a match. On success it sets the target to the matched value. A caller can then inspect fields on a particular error type without requiring that type to be the outermost error.

For instance, a caller receiving a wrapped file error can use a pointer target.

var pathErr *os.PathError
if errors.As(err, &pathErr) {
    fmt.Println(pathErr.Op, pathErr.Path)
}

The target is a pointer to the variable that will receive the result. The implementation checks this requirement and panics for an invalid target, including a nil pointer. Treating As as if it merely accepted an example error value would misunderstand how it returns the matching value to the caller.

errors.Unwrap performs one step. It returns the result of an error's Unwrap method when that method has the required signature, otherwise nil. Code choosing this lower-level operation takes responsibility for any further traversal. Is and As supply the common chain searches.

A chain preserves selected information

An error chain does not require every implementation detail to become visible. The function author still decides whether to wrap a cause. Some functions may translate a lower-level failure into a different public error contract. Others may deliberately preserve a filesystem error so the caller can classify it.

I prefer making that choice at the boundary where the abstraction is defined. Accidentally exposing an implementation error can create a caller dependency that becomes awkward to change. Deliberately preserving it can avoid brittle string matching and keep operational decisions grounded in typed values.

The release adds the mechanism for this choice, not a rule that every error must wrap every cause. A caller can inspect only what a wrapper exposes. Plainly formatting an error does not create an inspectable chain, and adding a contextual message should not be mistaken for adopting the convention automatically.

Module authentication becomes ordinary build behavior

Go 1.13 also changes how module inputs are obtained and authenticated. The default GOPROXY is https://proxy.golang.org,direct. The proxy list has specific fallback semantics, with a 404 or 410 allowing the command to try the next entry. An unreachable proxy or a different HTTP error terminates the search.

That behavior matters when configuring a build network. A temporary server error does not have the same meaning as a proxy reporting that a module is absent. Assuming that the presence of direct makes every proxy problem fall back to the origin would misread the release notes.

The new GOPRIVATE variable identifies module paths that are not public. It supplies defaults for the lower-level GONOPROXY and GONOSUMDB controls. This lets private modules avoid public proxy and checksum requests without giving up the corresponding defaults for public code.

The checksum database is consulted for modules not already represented in the main module's go.sum. Setting GOSUMDB=off disables that consultation while retaining verification against existing local checksums. The distinction is useful when explaining exactly what a restricted build configuration does and which evidence remains available.

Module mode itself still defaults through GO111MODULE=auto. The change is that auto now recognizes a containing go.mod even inside GOPATH/src. The proxy defaults should therefore not be described as if this release replaced the module-mode setting with unconditional on.

Language versions remain explicit

The number-literal changes include binary and explicit octal prefixes, hexadecimal floating-point values and underscores separating digits. Signed shift counts no longer require artificial unsigned conversions solely to satisfy the earlier language restriction. A module using these changes needs a language version of at least 1.13 in its go.mod file.

The toolchain can update that directive with go mod edit -go=1.13. This is a source-language selection, distinct from the proxy and checksum configuration used to obtain dependencies. Combining the two into one upgrade setting would conceal how different programs can behave under the same installed toolchain.

The release also lets go version inspect executables, with -m showing embedded module information when available. That gives operators a documented way to ask which Go version and module metadata an executable contains. It is more useful evidence about a running artifact than the version of an unrelated compiler installed nearby.

Runtime reporting changes alongside library behavior

The release notes describe more aggressive return of unused memory to the operating system after the heap shrinks. Earlier behavior could retain it for five or more minutes following a heap-size spike. Go 1.13 begins returning it promptly, but the notes distinguish that action from the operating system reclaiming the memory.

On Linux and other systems with lazy reclamation, reported resident memory may remain high until memory pressure causes reclamation. A memory chart can therefore lag behind the runtime's release of memory. Interpreting a toolchain upgrade requires keeping runtime decisions and operating-system accounting separate.

Out-of-range panics also gain the offending index and the slice length or capacity in their messages. This is a smaller change with a direct diagnostic benefit, because the failure report includes values that previously had to be reconstructed. The release retains the underlying panic behavior while providing more specific information about the failed access.

With error wrapping, the corresponding evidence is inside the returned value. A caller can retain the context supplied by intermediate operations and still find a deliberately exposed cause. The new library functions define how to inspect that chain, leaving the function author responsible for deciding which underlying errors belong in its public contract.