AlgoInsight

Q&A

How to handle errors in Golang

By AlgoInsight Team2026-06-07go

Overview

Unlike languages with try/catch, Go treats errors as values returned from functions. This makes error handling explicit and visible in the code flow - you can't accidentally ignore that a function might fail, because the error is right there in the return signature. The idiomatic pattern is the ubiquitous if err != nil check.

graph TD
    A["result, err := doSomething()"] --> B{err != nil?}
    B -->|yes| C[Handle: return/log/wrap]
    B -->|no| D[Use result, continue]
    C --> E["Wrap with fmt.Errorf(...%w...)"]
    E --> F["Inspect later: errors.Is / errors.As"]
    style B fill:#1e1b4b,color:#fff

The Basic Pattern

result, err := strconv.Atoi("123")
if err != nil {
    // handle the error
    return fmt.Errorf("failed to parse: %w", err)
}
fmt.Println(result)

error is a built-in interface:

type error interface {
    Error() string
}

Anything with an Error() string method is an error.

Creating Errors

import "errors"

err := errors.New("something went wrong")              // simple
err := fmt.Errorf("user %d not found", id)              // formatted
err := fmt.Errorf("loading config: %w", underlying)     // WRAP another error

The %w verb wraps an underlying error, preserving it for later inspection while adding context.

Inspecting Errors: errors.Is and errors.As

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

if errors.Is(err, ErrNotFound) {       // is this (or a wrapped) ErrNotFound?
    // handle the not-found case
}

var pathErr *os.PathError
if errors.As(err, &pathErr) {          // is there a *os.PathError in the chain?
    fmt.Println(pathErr.Path)          // access its fields
}

These work through %w-wrapped chains, which is why wrapping is preferred over just reformatting the message.

Custom Error Types

type ValidationError struct {
    Field string
    Msg   string
}
func (e *ValidationError) Error() string {
    return fmt.Sprintf("%s: %s", e.Field, e.Msg)
}

// usage
return &ValidationError{Field: "email", Msg: "is required"}

Custom types let callers use errors.As to access structured fields.

Sentinel Errors

Predefined error values you can compare against:

var ErrInsufficientFunds = errors.New("insufficient funds")
// caller:
if errors.Is(err, ErrInsufficientFunds) { /* ... */ }

panic / recover (use sparingly)

panic aborts normal flow; recover (inside a deferred function) can stop a panic from crashing the program. Reserve these for unrecoverable programmer errors or to convert a panic at a package boundary - not for ordinary error handling.

func safeRun() {
    defer func() {
        if r := recover(); r != nil {
            log.Println("recovered:", r)
        }
    }()
    panic("boom")
}

Best Practices

Why This Matters in Interviews

Go interviews focus heavily on the errors-as-values philosophy. Know the if err != nil idiom, the difference between errors.New and fmt.Errorf, what %w does (wrapping), and errors.Is vs errors.As. Crucially, articulate why Go avoids exceptions (explicitness) and why panic is not for normal errors.

Common Pitfalls

References

More answers