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}
Last modified: 27/10/2025 2022-2025 ©ainsley.dev, All rights reserved.