Errors
- Always check errors, never ignore them with
_unless absolutely necessary. - If ignoring an error, add a comment explaining why.
- Return errors up the stack; don’t just log and continue unless appropriate.
- Always prioritise clarity over depth of stack trace — add context that helps debugging, not repetition
Domain Error Types
Define custom errors to give context and allow type-based handling, rather than using generic fmt.Errorf. Use custom
errors only when it makes sense—for domain-specific cases where inspecting or handling by type is useful.
Example:
1type ErrInsufficientBalance struct {
2 Amount float64
3}
4
5func (e ErrInsufficientBalance) Error() string {
6 return fmt.Sprintf("insufficient balance: need %.2f", e.Amount)
7}
8
9// Usage
10if balance < withdrawAmount {
11 return ErrInsufficientBalance{Amount: withdrawAmount}
12}
Using errors.Wrap
Always use errors.Wrap from github.com/pkg/errors for adding context to errors. Use fmt.Errorf
if there are more than one argument that’s not an error.
I.e. you can use fmt.Errorf, but only when needing to format.
Example:
1func LoadConfig(fs afero.Fs, path string) (*Config, error) {
2 data, err := afero.ReadFile(fs, path)
3 if err != nil {
4 return nil, errors.Wrap(err, "reading config file")
5 }
6 return parseConfig(data)
7}
8
9func ValidatePort(port int) error {
10 if port < 1024 || port > 65535 {
11 return fmt.Errorf("invalid port %d: must be between 1024 and 65535", port)
12 }
13 return nil
14}