Chapter 12: The Sound of Breaking Glass
Monday morning arrived with a heavy gray mist clinging to the city. Inside the archive, the silence was absolute, broken only by the rhythmic scrape-scrape-scrape of Eleanor’s bone folder smoothing a crease in an ancient map.
Ethan walked in, his umbrella dripping. He set a small bakery box on the desk. "Pear and almond tart. And a Flat White, extra foam."
Eleanor paused her work. She inspected the tart. "A classic combination. The sweetness of the pear balances the bitterness of the almond. Well chosen."
Ethan sat down, opening his laptop with a sigh that was a little too loud.
"That sound," Eleanor said without looking up. "That is the sigh of a programmer fighting the compiler."
"Not the compiler," Ethan corrected. "The boilerplate. I'm writing this file parser, and half my code is just checking for errors. It feels… primitive. I miss exceptions. I miss try-catch blocks where I can just wrap everything in a bubble and handle problems at the end."
He turned his screen to her.
func ParseFile(filename string) {
f, err := os.Open(filename)
if err != nil {
log.Fatal(err)
}
// ... logic continues
}
"I was thinking," Ethan said. "Maybe I should just panic when things go wrong and recover at the top level? Like a global exception handler?"
Eleanor set down her bone folder. Her expression hardened.
"Ethan, imagine you are reading a book," she began. "You are on page 50. Suddenly, without warning, you are teleported to page 200. You have no idea how you got there or what happened in between. That is an Exception."
She took a sip of the Flat White. "Exceptions are invisible control flow. They allow a function to jump the stack, bypassing return statements and cleanup logic. In Go, we value visibility over convenience. We treat errors as Values."
Errors are just Values
She opened a new file. "An error in Go is not a magical event. It is a value, just like an integer or a string. It is a simple interface:"
type error interface {
Error() string
}
"It is just anything that can describe itself as a string. Because it is a value, you can pass it, store it, wrap it, or ignore it—though you certainly shouldn't."
She typed a new example. "You said you wanted to panic. Let me show you why treating errors as values is better."
package main
import (
"errors"
"fmt"
"os"
)
// Define a "Sentinel Error" - a constant value we can check against
var ErrEmptyConfig = errors.New("config file is empty")
func loadConfig(path string) (string, error) {
if path == "" {
// We create the error value right here
return "", errors.New("path cannot be empty")
}
file, err := os.Open(path)
if err != nil {
// We return the error value up the stack
return "", err
}
defer file.Close()
stat, err := file.Stat()
if err != nil {
return "", err
}
if stat.Size() == 0 {
// We return our specific sentinel error
return "", ErrEmptyConfig
}
return "config_data", nil
}
func main() {
_, err := loadConfig("missing.txt")
if err != nil {
fmt.Println("Error occurred:", err)
}
}
"Look at the flow," Eleanor pointed. "There are no hidden jumps. The error travels up the stack through return values. You can see exactly where it exits. This is Explicit Control Flow."
"But it's so verbose," Ethan grumbled. "if err != nil is everywhere."
"It is repetitive," Eleanor conceded. "But consider the alternative. If file.Stat() threw an exception, would you remember to close the file? In Go, the defer file.Close() runs no matter what errors happen later. The explicit checks force you to decide: 'What do I do if this fails right now?'"
Decorating the Error
"However," Eleanor added, "simply returning err is often lazy. If loadConfig returns 'file not found', the caller doesn't know which file. You need to add context."
She modified the code:
func loadConfig(path string) (string, error) {
file, err := os.Open(path)
if err != nil {
// Wrap the error with context using %w
// We only wrap when we are adding useful information.
return "", fmt.Errorf("failed to open config at %s: %w", path, err)
}
// ...
}
"The %w verb stands for 'wrap'. It puts the original error inside a new one. It creates a chain."
Ethan frowned. "But if I wrap it, how do I check what the original error was? If I want to check for ErrEmptyConfig?"
"A brilliant question." Eleanor opened her notebook. "Before Go 1.13, this was hard. Now, we have errors.Is and errors.As."
Unwrapping the Mystery
She typed a robust error handling example:
package main
import (
"errors"
"fmt"
"io/fs"
)
func main() {
_, err := loadConfig("config.json")
if err != nil {
// 1. Check if it matches a specific value (Sentinel)
if errors.Is(err, ErrEmptyConfig) {
fmt.Println("Please provide a non-empty config file.")
return
}
// 2. Check if it matches a specific TYPE (like PathError)
var pathErr *fs.PathError
if errors.As(err, &pathErr) {
fmt.Println("File system error on path:", pathErr.Path)
return
}
// 3. Fallback
fmt.Println("Unknown error:", err)
}
}
"Think of errors.Is like checking equality. It looks through the layers of wrapping to see if ErrEmptyConfig is buried somewhere inside. Think of errors.As like a type assertion. It checks if there is an error of type *fs.PathError inside."
Ethan studied the code. "So I can wrap an error ten times, adding context at every layer, and errors.Is can still find the original cause?"
"Exactly. You preserve the root cause while adding the narrative of how it failed."
Don't Panic
"So when can I use panic?" Ethan asked.
"Almost never," Eleanor replied sternly. "Panic is for when the program is fundamentally broken and cannot continue—like running out of memory, or a developer mistake that makes the internal state invalid. It is not for 'file not found' or 'network timeout'."
She picked up the tart. "If you panic in a library I import, you crash my entire application. That is rude. Return an error. Let me decide if I want to crash."
Ethan watched the rain streak against the window. "It forces you to handle the unhappy path first."
"Yes. It aligns your code to the left," Eleanor said, using her hand to trace the shape of the code in the air. "The 'happy path'—the successful logic—stays minimally indented on the left side of the screen. The error handling nests to the right and returns early. It makes the logic easy to scan."
Eleanor took a bite of the tart. "An error is not an exception, Ethan. An exception says 'something unexpected happened.' An error says 'this is a possible outcome of this function.' And in software, failure is always a possible outcome."
She wiped a crumb from her lip. "Treat your errors with respect. Give them context. Check them specifically. And never, ever assume they won't happen."
Ethan closed his laptop lid. "No more try-catch."
"No more try-catch," Eleanor agreed. "Just values. Passed from hand to hand, until they are resolved."
Key Concepts from Chapter 12
Errors are Values: In Go, errors are not special control flow mechanisms. They are values implementing the error interface.
The error Interface:
type error interface {
Error() string
}
Sentinel Errors: Pre-defined global variables for specific errors (e.g., io.EOF, errors.New("some error")). Useful for simple equality checks.
Wrapping Errors: Using fmt.Errorf("... %w ...", err) wraps an error, adding context while preserving the original error underneath.
errors.Is: Checks if a specific error value exists anywhere in the wrapping chain. Use this instead of == when handling wrapped errors.
-
Example:
if errors.Is(err, io.EOF)
errors.As: Checks if an error of a specific type exists anywhere in the chain, and assigns it to a variable.
var pathErr *fs.PathError // Note: must be a pointer to the error type
if errors.As(err, &pathErr) {
fmt.Println(pathErr.Path)
}
Panic vs. Error:
- Error: Expected failure modes (I/O, validation, network). Handled by returning values.
- Panic: Unexpected, unrecoverable state (nil pointer, index out of range). Crashes the program (unless recovered, which is rare).
- Rule: Don't panic in libraries. Return errors.
Align the Happy Path: Structure code to handle errors early and return. Keep the successful logic minimally indented.
Next chapter: Testing—where Eleanor shows Ethan that writing tests isn't a chore, but the only way to prove his system actually works.
Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.
Top comments (0)