Skip to content

proposal: context: support semantic error mapping by allowing Cause to return a wrapped parent error #74715

@OkutaniDaichi0106

Description

@OkutaniDaichi0106

Proposal Details

Support semantic error mapping by allowing Cause to return a wrapped parent error


Abstract

This proposal suggests allowing context.Cause to return a wrapped error of the parent context's cause. This enables libraries and applications to map or translate cancellation reasons semantically, rather than only propagating raw errors from lower-level dependencies such as network libraries.


Background

Currently, context.Cause(ctx) returns the raw error passed via WithCancelCause. This works well for many use cases, but when a context is canceled due to a lower-level cause (e.g., QUIC stream reset), higher-level code has no way to reclassify or semantically wrap the error for its own domain. This makes it hard to provide meaningful error types in user-facing APIs.


Motivation

In many layered systems, libraries and applications often need to map or translate low-level cancellation causes into higher-level, domain-specific errors. When a context is canceled due to an error from a lower-level dependency (such as a network or transport library), there is no standard way to wrap or reclassify that error within the context mechanism. This limitation makes it difficult for higher-level code to provide meaningful error types or to handle errors in a way that is appropriate for its own abstraction layer. Enabling semantic error wrapping would improve error reporting, debugging, and user experience in complex, multi-layered applications.


Proposal

The proposal is to allow context.Cause to return a wrapped error if the context implements a Cause() error method, enabling semantic mapping of cancellation causes.

package context

func Cause(c Context) error {
+    // If the context has a specific cause, return it.
+    if cc, ok := c.(interface{ Cause() error }); ok {
+        cause := cc.Cause()
+        return cause
+    }
    if cc, ok := c.Value(&cancelCtxKey).(*cancelCtx); ok {
        cc.mu.Lock()
        defer cc.mu.Unlock()
        return cc.cause
    }
    // There is no cancelCtxKey value, so we know that c is
    // not a descendant of some Context created by WithCancelCause.
    // Therefore, there is no specific cause to return.
    // If this is not one of the standard Context types,
    // it might still have an error even though it won't have a cause.
    return c.Err()
}

Example Use Case

package protocol

type Error struct {
    // Original error from transport layer
    transportError transport.Error
}

type contextWrapper struct {
    transportCtx context.Context
}

func (c *contextWrapper) Cause() error {
    cause := context.Cause(c.transportCtx)

    var transportError *transport.Error
    if errors.As(cause, &transportError) {
        return Error{
            transportError: *transportError,
        }
    }
    return cause
}
package main

transportCtx := transport.NewContext(context.Background())
protocolCtx := protocol.NewContext(transportCtx)

// User code can check:
err := context.Cause(protocolCtx)
var protocolError *protocol.Error
if errors.As(err, &protocolError) {
    // handle application-specific error
}

Rationale

  • Uses existing Go error semantics (errors.Is, errors.As, errors.Unwrap) to preserve compatibility.
  • Avoids complex error plumbing outside of context.
  • Encourages clearer error handling in layered libraries and protocols.

Compatibility

This proposal is backwards-compatible. It adds optional wrapping for enhanced semantics and does not change existing behavior.


Drawbacks / Alternatives

  • Using custom context keys and error values (not idiomatic, makes composition difficult)
  • Creating per-layer error plumbing manually (adds complexity, increases risk of mistakes)
  • Chaining contexts to propagate custom errors (deep context trees, higher memory usage, unpredictable cancellation propagation, and harder context management)

Conclusion

Enabling wrapped errors via context.Cause will improve the ability to build layered and semantically rich libraries while preserving Go's simplicity and idioms around context and error handling.

Metadata

Metadata

Assignees

No one assigned

    Labels

    LibraryProposalIssues describing a requested change to the Go standard library or x/ libraries, but not to a toolProposal

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions