Go Programming Series: This lesson is part of our comprehensive Complete Go Tutorial: Learn Go (Golang) from Beginner to Advanced.

Go Error Handling: Errors, Wrapping, Sentinel Errors and Robust Error Design

Go treats errors as ordinary values and makes failure handling explicit. This guide explains the error interface, creating and returning errors, wrapping and unwrapping, sentinel and typed errors, errors.Is and errors.As, context-aware failures, API boundaries, logging, testing and production practices.

What You Will Learn

  • Core Go rules and terminology
  • Practical, idiomatic examples
  • Design decisions and trade-offs
  • Edge cases and common mistakes
  • Testing, performance and production practices

At a Glance

Area Focus
Language Rules and mental model
Practice Runnable examples
Design APIs and maintainability
Reliability Testing and edge cases
Production Security and operational concerns

1. What Is an Error in Go?

The error interface represents an operation that failed. Its deliberately small contract makes it possible for packages to return simple errors, typed errors, wrapped errors and domain-specific failures through the same mechanism.

2. Why Go Uses Explicit Error Values

Go normally treats operational failures as values returned by functions rather than exceptions hidden from ordinary control flow. This makes the failure path visible at the call site.

3. The Standard Error Pattern

Functions often return a result together with an error. Callers should check the error before using the result when the contract says the result may be invalid on failure.

Practical Example

func loadUser(id int) (User, error) {
    user, err := repository.Find(id)
    if err != nil {
        return User{}, fmt.Errorf("find user %d: %w", id, err)
    }
    return user, nil
}

Keep the example small while learning. In production, place the behavior behind a clear package boundary and test the important success and failure paths.

4. Creating and Returning Errors

errors.New and fmt.Errorf cover many cases. Error messages should explain the failed operation and add useful context without leaking secrets or implementation details.

5. Checking Errors

The usual idiom is to check err immediately after the operation that returned it. Local checks keep the source and response to a failure close together.

6. Wrapping Errors

Wrapping with %w preserves an underlying cause while adding operation context. This lets higher layers understand both what operation failed and why.

7. errors.Is

errors.Is is used when callers need to recognize a stable error condition through an error chain. It is preferable to comparing error strings.

Practical Example

func loadUser(id int) (User, error) {
    user, err := repository.Find(id)
    if err != nil {
        return User{}, fmt.Errorf("find user %d: %w", id, err)
    }
    return user, nil
}

Keep the example small while learning. In production, place the behavior behind a clear package boundary and test the important success and failure paths.

8. errors.As

errors.As extracts a matching error type from an error chain. It is useful when structured information is required for a decision.

9. Sentinel Errors

Sentinel values represent stable conditions that callers may need to distinguish. They should be introduced only when that identity has real value to consumers.

10. Custom Error Types

Typed errors can carry fields such as resource identifiers, validation details or status information. Keep the type focused and avoid exposing private implementation details.

11. Error Chains

A chain preserves lower-level causes while allowing each layer to add useful context. Error strings remain human-oriented; Is and As provide programmatic inspection.

Practical Example

func loadUser(id int) (User, error) {
    user, err := repository.Find(id)
    if err != nil {
        return User{}, fmt.Errorf("find user %d: %w", id, err)
    }
    return user, nil
}

Keep the example small while learning. In production, place the behavior behind a clear package boundary and test the important success and failure paths.

12. Errors at Package Boundaries

A package should expose failures in terms meaningful to its consumers. Internal errors can be wrapped or translated so callers do not need to know private implementation details.

13. HTTP and API Error Mapping

HTTP handlers should translate internal failures into safe status codes and response bodies. An internal database or filesystem error should not automatically become the public response.

14. Logging Errors

Choose a logging ownership boundary. Logging every error at every layer creates duplicate noise. The final boundary that can act on the failure is often the best place for structured logging.

15. Transient and Permanent Failures

Some failures may be temporary while others are permanent. Retry logic should consider idempotency, backoff, maximum attempts and whether the underlying operation is safe to repeat.

16. Context and Errors

context.Context carries deadlines and cancellation. A function can respect context cancellation while still returning an error that explains the failed operation.

Practical Example

func loadUser(id int) (User, error) {
    user, err := repository.Find(id)
    if err != nil {
        return User{}, fmt.Errorf("find user %d: %w", id, err)
    }
    return user, nil
}

Keep the example small while learning. In production, place the behavior behind a clear package boundary and test the important success and failure paths.

17. Testing Error Behavior

Tests should prefer errors.Is, errors.As and observable behavior over exact string matching unless the message itself is an explicit API requirement.

18. Error Handling as an API Contract

Errors are part of an API's contract. Callers need to know which failures are expected, which can be retried, which represent missing resources, and which indicate programmer mistakes. A stable error contract is more valuable than a collection of loosely formatted messages.

19. Testing and Observability

A production-quality implementation should test both success and failure. Logs and metrics should help operators understand the failure without requiring the user-facing message to contain internal details.

20. Common Mistakes

Typical mistakes in this area include hiding failures, relying on implementation details, skipping cleanup, using advanced mechanisms when simple ones work, and failing to define behavior for boundary cases.

21. Best Practices

Keep the API explicit, use standard-library abstractions where possible, handle edge cases deliberately, test important failure paths, and isolate advanced behavior so ordinary application code stays easy to read.

22. Conclusion

The most useful Go code is not merely code that compiles; it is code whose behavior remains understandable when inputs fail, resources are limited, and the system evolves. Applying these principles turns the language feature into a reliable engineering tool.

Practical Decision Guide

Technique Use for Avoid when
errors.Is Stable conditions String comparison
errors.As Structured errors Scattered type checks
Wrapping Useful context Meaningless text
Typed error Structured data Simple failures

Engineering Workflow

Start with a minimal example, run it, and then introduce the edge case discussed in the section. Move the logic into a package with a clear API, add table-driven tests, and only then consider optimization or extra abstraction. This approach keeps the code understandable while developing production-level judgment.

Common Review Questions

Why should error strings not be parsed?

A strong answer should state the Go rule, show a small example, and explain the production trade-off. In review discussions, correctness, clarity, testability and failure behavior matter more than memorizing isolated definitions.

When should an error be wrapped?

A strong answer should state the Go rule, show a small example, and explain the production trade-off. In review discussions, correctness, clarity, testability and failure behavior matter more than memorizing isolated definitions.

What is the difference between errors.Is and errors.As?

A strong answer should state the Go rule, show a small example, and explain the production trade-off. In review discussions, correctness, clarity, testability and failure behavior matter more than memorizing isolated definitions.

Where should an error be logged?

A strong answer should state the Go rule, show a small example, and explain the production trade-off. In review discussions, correctness, clarity, testability and failure behavior matter more than memorizing isolated definitions.

Best-Practice Checklist

  • Prefer simple Go mechanisms before advanced abstractions.
  • Keep failure and resource-management paths explicit.
  • Validate assumptions at package boundaries.
  • Test normal and important boundary cases.
  • Do not expose internal implementation details unnecessarily.
  • Measure performance before optimizing.
  • Document behavior that callers must rely on.

Conclusion

The most useful Go code is not merely code that compiles; it is code whose behavior remains understandable when inputs fail, resources are limited, and the system evolves. Applying these principles turns the language feature into a reliable engineering tool.