Bird
Raised Fist0
Goprogramming~15 mins

Handling errors in Go - Deep Dive

Choose your learning style10 modes available
Overview - Handling errors
What is it?
Handling errors in Go means writing code that can detect when something goes wrong and respond properly. Instead of crashing or ignoring problems, Go programs check for errors and decide what to do next. This helps programs stay reliable and clear about what failed. Errors are values in Go, so you can pass them around and handle them like any other data.
Why it matters
Without proper error handling, programs can fail silently or crash unexpectedly, causing frustration and data loss. Handling errors lets programs recover gracefully or inform users clearly about issues. This makes software more trustworthy and easier to maintain. Imagine a calculator that just stops working without explanation; error handling prevents that kind of surprise.
Where it fits
Before learning error handling, you should understand basic Go syntax, functions, and variables. After mastering error handling, you can explore advanced topics like custom error types, error wrapping, and concurrency-safe error management.
Mental Model
Core Idea
Errors in Go are just values returned by functions that you must check and handle explicitly to keep your program running smoothly.
Think of it like...
Handling errors in Go is like checking the fuel gauge before a car trip; if you ignore it, you might run out of gas unexpectedly, but if you check and plan, you avoid getting stranded.
┌─────────────┐      ┌───────────────┐      ┌───────────────┐
│ Call Func   │─────▶│ Receive Result │─────▶│ Check for Err │
└─────────────┘      └───────────────┘      └───────────────┘
                                      │
                                      ▼
                             ┌─────────────────┐
                             │ Handle or Return │
                             └─────────────────┘
Build-Up - 7 Steps
1
FoundationWhat is an error in Go
🤔
Concept: Errors are values that describe something went wrong during a function's execution.
In Go, errors are represented by the built-in error type, which is an interface. Functions often return an error as the last return value. If the error is nil, it means no problem happened. If not nil, it means something went wrong. Example: func divide(a, b int) (int, error) { if b == 0 { return 0, errors.New("cannot divide by zero") } return a / b, nil }
Result
The function returns a result and an error value. If b is zero, the error is not nil and explains the problem.
Understanding that errors are just values helps you see error handling as a normal part of data flow, not a special case.
2
FoundationChecking errors after function calls
🤔
Concept: You must always check the error returned by a function before using the other results.
When you call a function that returns an error, you write code to test if the error is nil. If it is not nil, you handle it, for example by returning it or printing a message. Example: result, err := divide(10, 0) if err != nil { fmt.Println("Error:", err) return } fmt.Println("Result:", result)
Result
The program prints the error message and stops before using the invalid result.
Knowing to always check errors prevents bugs and crashes caused by ignoring problems.
3
IntermediateCreating custom error messages
🤔Before reading on: do you think you can create errors with any message you want? Commit to your answer.
Concept: You can create errors with custom messages to explain exactly what went wrong.
Go provides the errors.New function to create simple error values with a message. You can also use fmt.Errorf to format error messages with variables. Example: if b == 0 { return 0, fmt.Errorf("cannot divide %d by zero", a) }
Result
The error message includes the value of a, making it clearer what caused the error.
Custom error messages improve debugging and user feedback by giving precise information about failures.
4
IntermediateUsing multiple return values for errors
🤔Before reading on: do you think Go uses exceptions or multiple return values for errors? Commit to your answer.
Concept: Go uses multiple return values instead of exceptions to handle errors explicitly.
Unlike some languages that throw exceptions, Go functions return errors as a second value. This forces the programmer to handle errors immediately and clearly. Example: func readFile(name string) ([]byte, error) { data, err := os.ReadFile(name) if err != nil { return nil, err } return data, nil }
Result
The caller must check the error before using the data, making error handling visible and explicit.
Understanding Go's explicit error returns helps you write clearer and more reliable code by avoiding hidden failures.
5
IntermediatePropagating errors up the call stack
🤔Before reading on: do you think you should handle every error immediately or sometimes pass it up? Commit to your answer.
Concept: Sometimes you don't handle an error directly but pass it up to the caller to decide what to do.
If your function cannot fix the error, return it to the caller. This is called error propagation. Example: func processFile(name string) error { data, err := readFile(name) if err != nil { return err // pass error up } // process data return nil }
Result
The error moves up the call chain until some function handles it properly.
Knowing when to propagate errors keeps your code clean and lets higher-level functions decide how to respond.
6
AdvancedWrapping errors with additional context
🤔Before reading on: do you think errors can carry extra information beyond a message? Commit to your answer.
Concept: You can wrap errors to add context while preserving the original error for debugging.
Go 1.13 introduced error wrapping with fmt.Errorf and the %w verb. Example: if err != nil { return fmt.Errorf("reading config failed: %w", err) } This keeps the original error inside the new one, allowing unwrapping later.
Result
Errors show a chain of causes, making it easier to trace the root problem.
Wrapping errors helps maintain detailed error information across multiple layers of your program.
7
ExpertUsing sentinel and typed errors effectively
🤔Before reading on: do you think all errors should be compared by message text? Commit to your answer.
Concept: Sentinel errors are predefined error variables; typed errors are custom error types for precise error handling.
Sentinel errors let you compare errors by identity, not message. Example: var ErrNotFound = errors.New("not found") Typed errors implement the error interface with extra fields. Example: type MyError struct { Code int Msg string } func (e MyError) Error() string { return e.Msg } Using errors.Is and errors.As helps check wrapped errors correctly.
Result
You can write code that reacts differently to specific error types or sentinel values.
Understanding sentinel and typed errors prevents fragile error checks and enables robust error handling strategies.
Under the Hood
Go treats errors as values implementing the error interface, which has a single Error() string method. When a function returns an error, it is just returning a value like any other. The caller must check this value explicitly. Internally, error wrapping stores the original error inside a new error value, allowing unwrapping with errors.Unwrap. The runtime does not throw exceptions; it relies on explicit checks.
Why designed this way?
Go's designers wanted to avoid hidden control flow caused by exceptions, which can make code harder to follow and debug. Returning errors explicitly makes error handling visible and straightforward. This design trades off some verbosity for clarity and reliability. Alternatives like exceptions were rejected to keep the language simple and predictable.
┌───────────────┐
│ Function Call │
└──────┬────────┘
       │ returns
       ▼
┌───────────────┐
│ (Result, Err) │
└──────┬────────┘
       │
       ▼
┌───────────────┐
│ if Err != nil │
└──────┬────────┘
       │ handle or propagate
       ▼
┌───────────────┐
│ Continue Flow │
└───────────────┘
Myth Busters - 4 Common Misconceptions
Quick: Do you think ignoring an error in Go is safe if the program seems to work? Commit yes or no.
Common Belief:If the program runs fine, ignoring errors is okay and saves code clutter.
Tap to reveal reality
Reality:Ignoring errors can cause hidden bugs, data corruption, or crashes later because problems go unnoticed.
Why it matters:Silent failures make debugging very hard and can cause serious issues in production software.
Quick: Do you think Go uses exceptions like Java or Python? Commit yes or no.
Common Belief:Go handles errors with exceptions that jump out of functions automatically.
Tap to reveal reality
Reality:Go does not have exceptions; it uses explicit error return values that must be checked.
Why it matters:Expecting exceptions leads to missing error checks and fragile code.
Quick: Do you think comparing error messages as strings is a good way to check errors? Commit yes or no.
Common Belief:Comparing error text strings is the best way to identify specific errors.
Tap to reveal reality
Reality:Comparing error strings is fragile; errors should be compared by sentinel variables or types using errors.Is and errors.As.
Why it matters:String comparisons break easily if messages change, causing incorrect error handling.
Quick: Do you think wrapping errors loses the original error information? Commit yes or no.
Common Belief:Wrapping errors replaces the original error, so you lose details about the root cause.
Tap to reveal reality
Reality:Wrapping preserves the original error inside, allowing you to unwrap and inspect it later.
Why it matters:Knowing this helps maintain detailed error chains for better debugging.
Expert Zone
1
Errors can carry stack traces if you use third-party packages, but Go's standard errors do not include them by default.
2
Using errors.Is and errors.As correctly requires understanding how error wrapping works internally.
3
Returning nil for error means success, but returning a non-nil error with zero values for other returns is a common pattern to signal failure clearly.
When NOT to use
Explicit error returns are not suitable for truly exceptional, unrecoverable conditions where panics are more appropriate. For example, programmer mistakes or corrupted internal state should use panic instead. Also, for very complex error handling, consider structured logging or error reporting tools instead of just returning errors.
Production Patterns
In production Go code, errors are often wrapped with context to aid debugging. Functions return errors up the call stack until a top-level handler logs or recovers. Sentinel errors and typed errors are used to distinguish error causes precisely. Libraries often define their own error types to allow callers to react differently depending on the error.
Connections
Exception handling in other languages
Contrast with Go's explicit error returns
Understanding Go's approach clarifies why explicit checks improve code clarity compared to hidden exceptions.
Functional programming's Either/Result types
Similar pattern of returning success or failure values
Knowing Go errors as values connects to functional patterns that avoid exceptions by encoding errors in return types.
Human decision-making under uncertainty
Handling errors is like checking for problems before acting
Recognizing errors explicitly mirrors how people assess risks and decide carefully, improving reliability.
Common Pitfalls
#1Ignoring the error return value
Wrong approach:result, _ := divide(10, 0) fmt.Println("Result:", result)
Correct approach:result, err := divide(10, 0) if err != nil { fmt.Println("Error:", err) return } fmt.Println("Result:", result)
Root cause:Misunderstanding that errors must be checked explicitly leads to ignoring them and using invalid results.
#2Comparing error messages as strings
Wrong approach:if err.Error() == "not found" { // handle error }
Correct approach:if errors.Is(err, ErrNotFound) { // handle error }
Root cause:Treating error messages as identifiers is fragile and breaks if messages change.
#3Returning nil error but invalid data
Wrong approach:func divide(a, b int) (int, error) { if b == 0 { return 0, nil } return a / b, nil }
Correct approach:func divide(a, b int) (int, error) { if b == 0 { return 0, errors.New("cannot divide by zero") } return a / b, nil }
Root cause:Confusing no error with invalid operation causes silent failures.
Key Takeaways
In Go, errors are normal values returned by functions that must be checked explicitly to keep programs reliable.
Ignoring errors or treating them like exceptions leads to fragile and unpredictable code.
Custom error messages and wrapping provide detailed context that helps debugging and maintenance.
Using sentinel and typed errors allows precise error handling beyond simple message checks.
Understanding Go's error handling design helps write clear, robust, and maintainable software.

Practice

(1/5)
1. In Go, how do you check if a function returned an error?
easy
A. By checking if the error variable is zero
B. By checking if the error variable is an empty string
C. By checking if the error variable equals false
D. By checking if the error variable is not nil

Solution

  1. Step 1: Understand error type in Go

    Errors in Go are values, and when a function returns an error, it returns a value of type error or nil if no error occurred.
  2. Step 2: Check error value

    To know if an error happened, you check if the error variable is not nil, meaning an error value exists.
  3. Final Answer:

    By checking if the error variable is not nil -> Option D
  4. Quick Check:

    error != nil = true means error occurred [OK]
Hint: Always check if error != nil to detect errors [OK]
Common Mistakes:
  • Checking error == 0 instead of error != nil
  • Assuming error is a boolean
  • Comparing error to empty string
2. Which of the following is the correct way to declare and check an error returned by a function doTask() in Go?
easy
A. error := doTask() if error != false { // handle error }
B. err := doTask() if err != nil { // handle error }
C. err := doTask() if err == 0 { // handle error }
D. var err doTask() if err == nil { // handle error }

Solution

  1. Step 1: Declare error variable correctly

    Use short variable declaration with := to assign the error returned by doTask() to err.
  2. Step 2: Check if error is not nil

    Use if err != nil to check if an error occurred and handle it inside the block.
  3. Final Answer:

    err := doTask() if err != nil { // handle error } -> Option B
  4. Quick Check:

    Use := and err != nil for error handling [OK]
Hint: Use err := func() and check err != nil [OK]
Common Mistakes:
  • Using wrong variable declaration syntax
  • Checking error against 0 or false
  • Using reserved word 'error' as variable name
3. What will be the output of this Go code?
package main
import (
    "errors"
    "fmt"
)
func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}
func main() {
    result, err := divide(10, 0)
    if err != nil {
        fmt.Println(err)
    } else {
        fmt.Println(result)
    }
}
medium
A. division by zero
B. 0
C. 10
D. runtime error

Solution

  1. Step 1: Analyze divide function behavior

    If b is zero, divide returns 0 and an error with message "division by zero".
  2. Step 2: Check main function output

    Since divide(10, 0) returns an error, err != nil is true, so fmt.Println(err) prints the error message.
  3. Final Answer:

    division by zero -> Option A
  4. Quick Check:

    Error message prints when err != nil [OK]
Hint: If error returned, print error message, not result [OK]
Common Mistakes:
  • Assuming result prints even if error exists
  • Ignoring error check and printing zero
  • Expecting runtime panic instead of error
4. Identify the error in this Go code snippet:
package main
import (
    "fmt"
    "os"
)
func readFile() error {
    file, err := os.Open("data.txt")
    if err != nil {
        return err
    }
    defer file.Close()
    // read file contents
    return nil
}

func main() {
    err := readFile()
    if err != nil {
        fmt.Println("File read successfully")
    }
}
medium
A. The error check in main should be err == nil, not err != nil
B. The defer statement is misplaced and should be before error check
C. The function readFile should return a string, not error
D. os.Open does not return an error

Solution

  1. Step 1: Examine the error handling in main

    The main function calls readFile() and prints "File read successfully" if err != nil.
  2. Step 2: Identify the logic error

    err != nil means an error occurred, so printing success here is incorrect. Success should be printed only if err == nil (no error).
  3. Final Answer:

    The error check in main should be err == nil, not err != nil -> Option A
  4. Quick Check:

    Print success only if err == nil [OK]
Hint: Print success only when err == nil, not err != nil [OK]
Common Mistakes:
  • Confusing err == nil and err != nil conditions
  • Moving defer before error check incorrectly
  • Expecting os.Open to never return error
5. You want to write a Go function that tries to open a file and returns a custom error message if it fails. Which code snippet correctly wraps the original error with a custom message using Go's standard library?
hard
A. file, err := os.Open("config.json") if err != nil { panic(err) } return file.Close()
B. file, err := os.Open("config.json") if err != nil { return errors.New("failed to open config") } return file.Close()
C. file, err := os.Open("config.json") if err != nil { return fmt.Errorf("failed to open config: %w", err) } return file.Close()
D. file, err := os.Open("config.json") if err != nil { return err.Error() } return file.Close()

Solution

  1. Step 1: Understand error wrapping in Go

    Go 1.13+ supports wrapping errors with fmt.Errorf and %w verb to keep original error context.
  2. Step 2: Check each option for correct wrapping

    file, err := os.Open("config.json") if err != nil { return fmt.Errorf("failed to open config: %w", err) } return file.Close() uses fmt.Errorf with %w to wrap original error, preserving it. file, err := os.Open("config.json") if err != nil { return errors.New("failed to open config") } return file.Close() creates new error losing original error. file, err := os.Open("config.json") if err != nil { panic(err) } return file.Close() panics instead of returning error. file, err := os.Open("config.json") if err != nil { return err.Error() } return file.Close() returns string instead of error type.
  3. Final Answer:

    file, err := os.Open("config.json") if err != nil { return fmt.Errorf("failed to open config: %w", err) } return file.Close() -> Option C
  4. Quick Check:

    Use fmt.Errorf with %w to wrap errors [OK]
Hint: Use fmt.Errorf("%w") to wrap errors with context [OK]
Common Mistakes:
  • Replacing original error instead of wrapping
  • Using panic instead of returning error
  • Returning error message string instead of error type
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard