-
Notifications
You must be signed in to change notification settings - Fork 18.3k
Description
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.