# Go Error Handling in 2026: Patterns, Wrapping and Technical Interview Questions > Go error handling patterns and best practices: sentinel errors, custom error types, errors.Is, errors.As, error wrapping with fmt.Errorf %w, and common interview questions. - Published: 2026-06-15 - Updated: 2026-06-15 - Author: SharpSkill - Tags: go, error-handling, best-practices, interview, golang - Reading time: 9 min --- Go error handling sets the language apart from most mainstream alternatives. Instead of exceptions and try/catch blocks, Go treats errors as values—returned explicitly from functions and checked at every call site. This deliberate design choice forces error handling into the foreground, making failure paths visible and testable. > **Error Values, Not Exceptions** > > Go has no exception mechanism. Every function that can fail returns an error as its last return value. This pattern makes the cost of ignoring errors visible at the source level—and makes error flows trivially testable. ## The Error Interface and Why It Matters The entire Go error system rests on a single interface defined in the [standard library](https://pkg.go.dev/builtin#error): ```go // builtin.go type error interface { Error() string } ``` Any type implementing `Error() string` satisfies this interface. This simplicity drives composability: errors can be structs, wrappers, or anything else, as long as they produce a string representation. A basic custom error type demonstrates the pattern: ```go // apperror.go type NotFoundError struct { Resource string ID string } func (e *NotFoundError) Error() string { return fmt.Sprintf("%s with ID %s not found", e.Resource, e.ID) } ``` This `NotFoundError` carries structured data. Callers can extract the resource name or ID programmatically instead of parsing a string—a critical advantage when building HTTP APIs or CLI tools that map errors to specific responses. ## Sentinel Errors for Well-Known Conditions Sentinel errors are package-level variables representing specific, well-known failure conditions. The standard library uses this pattern extensively: `io.EOF`, `sql.ErrNoRows`, `os.ErrNotExist`. ```go // errors.go var ( ErrNotFound = errors.New("record not found") ErrUnauthorized = errors.New("unauthorized access") ErrConflict = errors.New("resource conflict") ) ``` Sentinel errors work best when the caller only needs to know *what* failed, not *why* in detail. They signal a condition without carrying additional context. The Go convention is to prefix them with `Err` and keep the message lowercase without punctuation, following the [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments#error-strings) guideline. > **When Sentinels Become a Problem** > > Exporting sentinel errors creates a public API contract. Downstream packages will match against them with `errors.Is`, so renaming or removing a sentinel error is a breaking change. Use them sparingly—only for conditions callers genuinely need to branch on. ## Error Wrapping with fmt.Errorf and the %w Verb Error wrapping, introduced in Go 1.13, adds context to an error while preserving the original in a chain. The `%w` verb in `fmt.Errorf` creates this chain: ```go // repository.go func (r *UserRepo) FindByID(ctx context.Context, id string) (*User, error) { user, err := r.db.QueryContext(ctx, "SELECT * FROM users WHERE id = $1", id) if err != nil { return nil, fmt.Errorf("UserRepo.FindByID(%s): %w", id, err) } return user, nil } ``` The wrapped error retains the full chain. A caller three layers up can still match the original sentinel or extract the original type. Each layer adds context about *where* the error occurred without obscuring *what* happened. A critical distinction: `%w` wraps (preserving the chain), while `%v` formats the error as a string and breaks the chain. Use `%v` deliberately when the original error should not leak through the abstraction boundary—for instance, when a repository wraps a database driver error that service-layer code should never inspect. ## Inspecting Errors with errors.Is and errors.As The `errors` package provides two functions for inspecting wrapped error chains, replacing direct comparison and type assertion. `errors.Is` walks the chain looking for a specific error value: ```go // handler.go func handleGetUser(w http.ResponseWriter, r *http.Request) { user, err := userService.GetByID(r.Context(), chi.URLParam(r, "id")) if errors.Is(err, ErrNotFound) { // Matches even if ErrNotFound was wrapped multiple times http.Error(w, "User not found", http.StatusNotFound) return } if err != nil { http.Error(w, "Internal error", http.StatusInternalServerError) return } json.NewEncoder(w).Encode(user) } ``` `errors.As` extracts a specific error type from the chain: ```go // middleware.go func errorMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if r := recover(); r != nil { http.Error(w, "Internal error", http.StatusInternalServerError) } }() next.ServeHTTP(w, r) }) } func mapErrorToHTTP(err error) int { var notFound *NotFoundError if errors.As(err, ¬Found) { // notFound.Resource and notFound.ID are available return http.StatusNotFound } var validationErr *ValidationError if errors.As(err, &validationErr) { return http.StatusBadRequest } return http.StatusInternalServerError } ``` The key advantage over type assertions: both functions traverse the entire wrapped chain. A `NotFoundError` wrapped three times with `fmt.Errorf` and `%w` still matches. ## Structured Error Handling in Layered Applications Production Go applications typically organize errors across three layers: domain errors define business-level conditions, service errors add operational context, and [handler or transport errors](https://www.datadoghq.com/blog/go-error-handling/) map them to external responses. ```go // domain/errors.go type DomainError struct { Code string // machine-readable: "USER_NOT_FOUND", "INVALID_INPUT" Message string // human-readable description Err error // original cause } func (e *DomainError) Error() string { if e.Err != nil { return fmt.Sprintf("%s: %s: %v", e.Code, e.Message, e.Err) } return fmt.Sprintf("%s: %s", e.Code, e.Message) } func (e *DomainError) Unwrap() error { return e.Err } ``` The `Unwrap` method is what makes `errors.Is` and `errors.As` work through the chain. Any custom error type that wraps another error should implement it. The service layer wraps domain errors with operational context: ```go // service/user.go func (s *UserService) Deactivate(ctx context.Context, userID string) error { user, err := s.repo.FindByID(ctx, userID) if err != nil { return fmt.Errorf("deactivating user %s: %w", userID, err) } if user.Status == StatusInactive { return &DomainError{ Code: "ALREADY_INACTIVE", Message: fmt.Sprintf("user %s is already inactive", userID), } } return s.repo.UpdateStatus(ctx, userID, StatusInactive) } ``` ## Common Interview Questions on Go Error Handling Technical interviews for Go positions frequently test error handling knowledge. These questions appear regularly in screening rounds and are covered in depth in [Go error handling interview questions](/technologies/go/interview-questions/error-handling). **Why does Go use error values instead of exceptions?** Exceptions create invisible control flow. A function that throws transfers control to an unknown catch site, potentially many stack frames away. Go error values make every failure path explicit in the function signature. The caller decides immediately how to handle the error—retry, wrap, log, or propagate. This eliminates the "surprise throw" problem and makes error flows readable without tooling support. **What is the difference between `%w` and `%v` in `fmt.Errorf`?** `%w` wraps the error, preserving the chain for `errors.Is` and `errors.As`. `%v` formats the error as a string, producing a new error with no chain link back to the original. Use `%w` when callers should be able to inspect the cause; use `%v` when the cause is an implementation detail that should not leak through an abstraction boundary. **When should a function return an error vs. panic?** Panic is reserved for truly unrecoverable situations: programming errors like index out of bounds, nil pointer dereferences, or violated invariants that indicate a bug. Recoverable failures—network timeouts, missing records, invalid input—return errors. A useful rule: if the condition can occur during normal operation, it returns an error. If it means the program has a bug, it panics. **How does the `Unwrap` method affect error chain inspection?** When a custom error type implements `Unwrap() error`, the `errors.Is` and `errors.As` functions follow that method to traverse the chain. Without `Unwrap`, the chain stops at that error. Since Go 1.20, errors can also implement `Unwrap() []error` to return multiple wrapped errors, enabling tree-shaped error chains for cases like aggregating validation errors. ## Error Handling Patterns to Avoid Several anti-patterns appear frequently in Go codebases and in [code review sessions](/blog/go/go-design-patterns-essential-patterns-interview): **Ignoring errors silently.** The blank identifier makes this syntactically easy but semantically dangerous: ```go // anti-pattern: silent error swallowing result, _ := riskyOperation() ``` Every ignored error should be a deliberate, documented choice—never a shortcut. **Logging and returning.** This pattern produces duplicate log entries and confuses the caller who also handles the error: ```go // anti-pattern: double handling if err != nil { log.Printf("operation failed: %v", err) // logged here return err // AND returned to caller who may log again } ``` The fix: either handle the error (log it, return a default, retry) *or* propagate it. Not both. **String matching for error classification.** Checking `err.Error()` output with `strings.Contains` is fragile. Error messages are not API contracts—they can change between library versions. Use `errors.Is` for sentinel values and `errors.As` for types. **Wrapping errors that cross goroutine boundaries without synchronization.** When multiple [goroutines](/blog/go/go-concurrency-goroutines-channels) produce errors concurrently, use `errgroup.Group` from the `golang.org/x/sync` package or a similar synchronization mechanism. Directly appending to a shared slice without a mutex creates data races. ## Error Handling with errgroup for Concurrent Operations The `errgroup` package from [golang.org/x/sync](https://pkg.go.dev/golang.org/x/sync/errgroup) provides a clean pattern for collecting errors from concurrent goroutines: ```go // batch.go func (s *OrderService) ProcessBatch(ctx context.Context, orderIDs []string) error { g, ctx := errgroup.WithContext(ctx) g.SetLimit(10) // limit concurrent goroutines for _, id := range orderIDs { g.Go(func() error { if err := s.processOrder(ctx, id); err != nil { return fmt.Errorf("processing order %s: %w", id, err) } return nil }) } // Returns the first non-nil error and cancels remaining work return g.Wait() } ``` `errgroup.WithContext` cancels the derived context when any goroutine returns an error, signaling the others to stop. This avoids wasted work and provides a single, wrapped error to the caller. > **Go 1.24 and Error Handling** > > Go 1.24 (February 2025) did not change the errors package. The error handling API has been stable since Go 1.13, with the Go 1.20 addition of multi-error unwrapping via `Unwrap() []error`. Current proposals for improved error handling syntax remain under discussion in the Go issue tracker. ## Conclusion - Treat errors as values: return them explicitly and handle them at every call site instead of relying on hidden exception mechanisms - Use sentinel errors (`var ErrX = errors.New(...)`) only for conditions callers need to branch on, and treat them as public API - Wrap errors with `fmt.Errorf` and `%w` to add context while preserving the chain; use `%v` to intentionally break the chain at abstraction boundaries - Prefer `errors.Is` over `==` and `errors.As` over type assertions—both traverse the full wrapped chain - Implement `Unwrap() error` on custom error types so chain inspection works correctly - Handle or propagate—never both. Logging an error and returning it produces duplicate noise - Use `errgroup` for concurrent error collection instead of manual goroutine synchronization - Keep error messages lowercase without punctuation, following Go conventions --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/en/blog/go/go-error-handling-patterns-wrapping-best-practices