Tutorials Logic, IN info@tutorialslogic.com

Golang Error Handling: errors, fmt.Errorf, wrapping and panic

Errors Are Values

Golang handles expected failures with ordinary return values. A function that can fail usually returns a useful result and an error. The caller checks whether the error is nil before using the result.

This explicit style makes failure paths visible. Instead of hidden exceptions jumping out of a function, the code shows exactly where errors are checked, wrapped, handled, logged, or returned to the caller.

Return Result and Error

Return Result and Error
package main

import (
    "fmt"
    "strconv"
)

func parseLimit(value string) (int, error) {
    limit, err := strconv.Atoi(value)
    if err != nil {
        return 0, fmt.Errorf("parse limit %q: %w", value, err)
    }

    return limit, nil
}

func main() {
    limit, err := parseLimit("25")
    if err != nil {
        fmt.Println("error:", err)
        return
    }

    fmt.Println("limit:", limit)
}

Creating Sentinel Errors

A sentinel error is a package-level error value used for a known condition. Common examples are not found, already exists, unauthorized, or invalid state.

Sentinel errors are useful when callers need to make decisions based on a specific failure. Use errors.Is to compare them, especially after wrapping.

Known Error Values

Known Error Values
package tutorials

import (
    "errors"
    "fmt"
)

var ErrNotFound = errors.New("tutorial not found")

func LoadTutorial(slug string) error {
    return fmt.Errorf("load tutorial %q: %w", slug, ErrNotFound)
}

func HandleTutorial(slug string) {
    err := LoadTutorial(slug)
    if errors.Is(err, ErrNotFound) {
        fmt.Println("show 404 page")
        return
    }
}

Adding Context with Wrapping

Use fmt.Errorf with %w to add context while preserving the original error. Context should explain what operation failed and include important values, such as an ID, filename, route, or user action.

Wrapping lets logs tell a story from high-level operation to low-level cause. The caller can still use errors.Is for known sentinel errors and errors.As for custom error types.

Tool Purpose
fmt.Errorf("...: %w", err) Add context while preserving the original error.
errors.Is(err, target) Check whether an error matches a known error value.
errors.As(err, &target) Extract a custom error type from an error chain.

Wrap at Boundaries

Wrap at Boundaries
func SaveProfile(userID int, input ProfileInput) error {
    if err := validateProfile(input); err != nil {
        return fmt.Errorf("validate profile for user %d: %w", userID, err)
    }

    if err := repository.Save(userID, input); err != nil {
        return fmt.Errorf("save profile for user %d: %w", userID, err)
    }

    return nil
}

Custom Error Types

A custom error type is useful when callers need structured details, not just a message. For example, a validation error may include the field name, a code, and a user-friendly message.

A type becomes an error when it implements the Error() string method. Keep the method message helpful for logs, and expose fields when code needs to make decisions.

Structured Validation Error

Structured Validation Error
type ValidationError struct {
    Field   string
    Message string
}

func (e ValidationError) Error() string {
    return e.Field + ": " + e.Message
}

func validateTitle(title string) error {
    if title == "" {
        return ValidationError{Field: "title", Message: "is required"}
    }
    return nil
}

func handle() {
    err := validateTitle("")
    var validationErr ValidationError
    if errors.As(err, &validationErr) {
        fmt.Println(validationErr.Field, validationErr.Message)
    }
}

Designing Error Messages

Good error messages explain the failed operation and important values. Keep messages lowercase and avoid punctuation unless the message contains multiple sentences. This convention helps wrapped errors read naturally.

Return errors at the right level. Low-level functions should describe the technical operation that failed. Higher-level functions can wrap with business context so the final error is useful in logs and debugging.

  • Prefer open config file: permission denied over Error!.
  • Include meaningful values such as IDs, slugs, paths, and operation names.
  • Do not match errors by string content for program decisions.
  • Avoid logging the same error at every layer; log at a boundary where action can be taken.

panic and recover

Use panic for programmer mistakes and impossible states, not for normal validation failures. A missing title in an API request should return an error response, not panic.

Some servers recover from panics at a boundary so they can log the issue and keep serving other requests. Even then, ordinary failures should still use error. Recovery is a safety net, not the main error-handling strategy.

Situation Use
Invalid user input Return an error or HTTP 400 response.
Missing database record Return a not-found error.
Impossible internal state Panic may be acceptable.
Server boundary Recover, log, and return a safe response.
Before you move on

Golang Error Handling: errors, fmt.Errorf, wrapping and panic Mastery Check

6 checks
  • Expected failures should be returned as error.
  • Return zero values with errors and check err != nil before using results.
  • Wrap errors with %w to preserve causes while adding context.
  • Use errors.Is and errors.As instead of string matching.
  • Custom error types are useful when callers need structured details.
  • Do not use panic for normal validation or business failures.
Next Step
Next Practice

Finish the concept here, then reinforce it with hands-on coding, interview prep, or a tool that matches the topic.

Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.