The go journey - Why I Rewrote My Go Package's Error Handling

The go journey - Why I Rewrote My Go Package's Error Handling
Photo by Greg Kubrak / Unsplash

Working on my temperature converter CLI, I hit a wall with error messages that told me nothing useful. A simple unknown unit X error could come from parsing user input, validating JSON, or internal conversion logic. Debugging meant stepping through code to figure out where the error actually happened.
Here's why I rewrote the entire error handling approach.

The Breaking Point
This error message was the last straw: unknown unit "X"

That's it. No context about:
- Which function threw the error?
- Was this from user input or internal logic?
- What operation was being attempted?
- Can the caller recover somehow?
When building a CLI that processes batch files with hundreds of temperature conversions, generic error messages become debugging nightmares


The New Approach: Structured Error Types
I created dedicated error types that carry context:


type ConversionError struct {
    Operation    string
    Unit  Unit     
    Value float64
    Err   error  
}

type ValidationError struct {
    Operation     string 
    Field  string 
    Value  string 
    Reason string 
}

Now the same validation error becomes:


func (unit Unit) Validate() error {
    switch unit {
    case UnitCelsius, UnitFahrenheit, UnitKelvin:
        return nil
    default:
        return &ValidationError{
            Op:     "unit validation",
            Field:  "unit", 
            Value:  string(unit),
            Reason: "must be one of C, F, or K",
        }
    }
}

The Dramatic Improvement

Before: unknown unit "X"
After: conversion error in validate source unit (unit=X): validation error in unit validation: field unit with value "X": must be one of C, F, or K

Now I know:
1. This happened during a conversion operation
2. Specifically during source unit validation
3. The unit was "X"
4. Exactly what the valid options are


Why This Matters Beyond Debugging:

Why This Matters Beyond Debugging
1. Better User Experience
Instead of cryptic errors, users get actionable feedback:


  if errors.Is(err, &ValidationError{}) {    
    fmt.Println("Tip: Use C for Celsius, F for Fahrenheit, or K for Kelvin")
  }
  


2. Programmatic Error Handling Different error types enable different recovery strategies:


var valErr *ValidationErrorif errors.As(err, &valErr) {  
  // suggest correction or retry with defaults?
}
var convErr *ConversionError  if errors.As(err, &convErr) {   
  // log metrics about conversion failing
}


3. Better Logging and Monitoring: Structured errors provide rich data for observability:


if err.Error() != "unknown unit \"X\"" {
  t.Error("wrong error message")
  }


var valErr *ValidationErrorif !errors.As(err, &valErr) || valErr.Field != "unit" {
  t.Error("expected unit validation error")
}


The Implementation Cost
More upfront code:
Creating error types requires more lines than fmt.Errorf().
Worth it: The debugging time saved in the first week paid for the extra implementation time.
Backwards compatible: Old error handling still works - this was purely additive.

When NOT to Do This
I don't use structured errors for:

- Prototype code: where iteration speed matters most
- Truly exceptional cases: that should panic anyway
- Simple operations: where generic errors provide enough context


The Ripple Effects
Structured errors unlocked other improvements I hadn't planned:
- Options pattern for cleaner configuration APIs
- Better CLI help text generated from error reasons
- Metrics collection using error operation data
- Retry logic that handles different error types differently


Key Takeaway
Generic error messages optimize for the writer's convenience. Structured error messages optimize for the reader's (debugger's, user's, monitoring system's) needs.
The extra code upfront pays dividends every time you need to understand what went wrong.

---
This is part of my Go learning journey building a temperature converter CLI. You can see the full code and other posts about package design and concurrency patterns in the [repository]