3.9 KiB
| title | description |
|---|---|
| Errors | The E() helper function and Error struct for contextual error handling. |
Errors
Core provides a standardised error type and constructor for wrapping errors with operational context. This makes it easier to trace where an error originated and provide meaningful feedback.
The Error Struct
type Error struct {
Op string // the operation, e.g. "config.Load"
Msg string // human-readable explanation
Err error // the underlying error (may be nil)
}
- Op identifies the operation that failed. Use the format
package.Functionorservice.Method. - Msg is a human-readable message explaining what went wrong.
- Err is the underlying error being wrapped. May be
nilfor root errors.
The E() Helper
E() is the primary way to create contextual errors:
func E(op, msg string, err error) error
With an Underlying Error
data, err := os.ReadFile(path)
if err != nil {
return core.E("config.Load", "failed to read config file", err)
}
This produces: config.Load: failed to read config file: open /path/to/file: no such file or directory
Without an Underlying Error (Root Error)
if name == "" {
return core.E("user.Create", "name cannot be empty", nil)
}
This produces: user.Create: name cannot be empty
When err is nil, the Err field is not set and the output omits the trailing error.
Error Output Format
The Error() method produces a string in one of two formats:
// With underlying error:
op: msg: underlying error text
// Without underlying error:
op: msg
Unwrapping
Error implements the Unwrap() error method, making it compatible with Go's errors.Is and errors.As:
originalErr := errors.New("connection refused")
wrapped := core.E("db.Connect", "failed to connect", originalErr)
// errors.Is traverses the chain
errors.Is(wrapped, originalErr) // true
// errors.As extracts the Error
var coreErr *core.Error
if errors.As(wrapped, &coreErr) {
fmt.Println(coreErr.Op) // "db.Connect"
fmt.Println(coreErr.Msg) // "failed to connect"
}
Building Error Chains
Because E() wraps errors, you can build a logical call stack by wrapping at each layer:
// Low-level
func readConfig(path string) ([]byte, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, core.E("config.readConfig", "failed to read file", err)
}
return data, nil
}
// Mid-level
func loadConfig() (*Config, error) {
data, err := readConfig("/etc/app/config.yaml")
if err != nil {
return nil, core.E("config.Load", "failed to load configuration", err)
}
// parse data...
return cfg, nil
}
// Top-level
func (s *Service) OnStartup(ctx context.Context) error {
cfg, err := loadConfig()
if err != nil {
return core.E("service.OnStartup", "startup failed", err)
}
s.config = cfg
return nil
}
The resulting error message reads like a stack trace:
service.OnStartup: startup failed: config.Load: failed to load configuration: config.readConfig: failed to read file: open /etc/app/config.yaml: no such file or directory
Conventions
- Op format: Use
package.Functionorservice.Method. Keep it short and specific. - Msg format: Use lowercase, describe what failed (not what succeeded). Write messages that make sense to a developer reading logs.
- Wrap at boundaries: Wrap with
E()when crossing package or layer boundaries, not at every function call. - Always return
error:E()returns theerrorinterface, not*Error. Callers should not need to know the concrete type. - Nil underlying error: Pass
nilforerrwhen creating root errors (errors that do not wrap another error).