Error Handling Patterns

One of the most defining characteristics of Go is its approach to error handling. Go does not use try/catch exceptions. Instead, it treats Errors as Values. This design forces developers to deal with errors as part of the normal execution flow, leading to more robust and predictable code.


1. The error Interface

An error in Go is simply any type that satisfies the built-in error interface:

type error interface {
    Error() string
}

2. The Standard Pattern: Return and Check

Most functions in Go return a result and an error as the final return value. The conventional way to handle this is the "Check and Return Early" pattern.

val, err := DoSomething()
if err != nil {
    return fmt.Errorf("could not do thing: %w", err)
}
// Proceed with success logic

3. Error Wrapping and Sentinel Errors

if errors.Is(err, os.ErrNotExist) {
    // Handle file not found
}

4. Custom Error Types

For complex errors that need to carry extra metadata (like an error code or a field name), you can define a custom struct that implements the Error() string method.

type APIError struct {
    Code    int
    Message string
}

func (e *APIError) Error() string {
    return fmt.Sprintf("API Error %d: %s", e.Code, e.Message)
}

Interview Pro-Tips: Why no Exceptions?

If an interviewer asks why Go doesn't have try/catch:


Technical Summary

  1. Validation: Errors are just data.
  2. Propagation: Always add context when returning an error up the stack.
  3. Best Practice: Avoid "If err != nil { return err }" boilerplate by using specialized error-handling patterns or custom types where it adds value.