Tutorials Logic, IN info@tutorialslogic.com
Navigation
Home About Us Contact Us Blogs FAQs
Tutorials
All Tutorials
Services
Academic Projects Resume Writing Website Development
Practice
Quiz Challenge Interview Questions Certification Practice
Tools
Online Compiler JSON Formatter Regex Tester CSS Unit Converter Color Picker
Compiler Tools

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
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
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.

ToolPurpose
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
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
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.

SituationUse
Invalid user inputReturn an error or HTTP 400 response.
Missing database recordReturn a not-found error.
Impossible internal statePanic may be acceptable.
Server boundaryRecover, log, and return a safe response.
Key Takeaways
  • 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.

Ready to Level Up Your Skills?

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