140 lines
3.9 KiB
Markdown
140 lines
3.9 KiB
Markdown
|
|
---
|
||
|
|
title: Errors
|
||
|
|
description: 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
|
||
|
|
|
||
|
|
```go
|
||
|
|
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.Function` or `service.Method`.
|
||
|
|
- **Msg** is a human-readable message explaining what went wrong.
|
||
|
|
- **Err** is the underlying error being wrapped. May be `nil` for root errors.
|
||
|
|
|
||
|
|
## The E() Helper
|
||
|
|
|
||
|
|
`E()` is the primary way to create contextual errors:
|
||
|
|
|
||
|
|
```go
|
||
|
|
func E(op, msg string, err error) error
|
||
|
|
```
|
||
|
|
|
||
|
|
### With an Underlying Error
|
||
|
|
|
||
|
|
```go
|
||
|
|
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)
|
||
|
|
|
||
|
|
```go
|
||
|
|
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`:
|
||
|
|
|
||
|
|
```go
|
||
|
|
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:
|
||
|
|
|
||
|
|
```go
|
||
|
|
// 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
|
||
|
|
|
||
|
|
1. **Op format**: Use `package.Function` or `service.Method`. Keep it short and specific.
|
||
|
|
2. **Msg format**: Use lowercase, describe what failed (not what succeeded). Write messages that make sense to a developer reading logs.
|
||
|
|
3. **Wrap at boundaries**: Wrap with `E()` when crossing package or layer boundaries, not at every function call.
|
||
|
|
4. **Always return `error`**: `E()` returns the `error` interface, not `*Error`. Callers should not need to know the concrete type.
|
||
|
|
5. **Nil underlying error**: Pass `nil` for `err` when creating root errors (errors that do not wrap another error).
|
||
|
|
|
||
|
|
## Related Pages
|
||
|
|
|
||
|
|
- [Services](services.md) -- services that return errors
|
||
|
|
- [Lifecycle](lifecycle.md) -- lifecycle error aggregation
|
||
|
|
- [Testing](testing.md) -- testing error conditions (`_Bad` suffix)
|