Go
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
151: Custom Error Types Revisited
We've talked about custom errors before, but usually, it's in a vacuum. You create a struct, give it an Error() method, and you're done. But in a real-world project—one with multiple layers of service calls and database wrappers—that simple approach usually falls apart the moment you start wrapping errors to add context.
I want to show you exactly where I usually trip up and how I fixed it. Let's imagine we're building a configuration loader for a system that reads a .toml file. We want to know not just that it failed, but specifically where it failed so we can tell the user exactly which line to fix.
Just a simple struct, right?
My first instinct is always to just define a custom type. It's clean and typed. Let's try this:
type ConfigError struct {
Line int
Msg string
}
func (e *ConfigError) Error() string {
return fmt.Sprintf("config error at line %d: %s", e.Line, e.Msg)
}
func LoadConfig() error {
// Simulating a parse error on line 42
return &ConfigError{Line: 42, Msg: "unexpected character '@'"}
}
Now, if I call LoadConfig() and use a type assertion, everything is great. I can grab that Line number and do something smart with it. But here is where the "real world" hits. In a real app, I wouldn't just return the error; I'd wrap it to provide context about which file was being loaded.
The moment it all breaks
Let's wrap that error and see what happens to our ability to inspect it:
func MainProcess() error {
err := LoadConfig()
if err != nil {
// I'm adding context here using %w
return fmt.Errorf("failed to initialize system: %w", err)
}
return nil
}
func main() {
err := MainProcess()
// Attempting the old-school type assertion
if cfgErr, ok := err.(*ConfigError); ok {
fmt.Println("Found the line number:", cfgErr.Line)
} else {
fmt.Println("Could not extract ConfigError!")
}
}
If you run this, it prints "Could not extract ConfigError!". Why? Because fmt.Errorf with %w doesn't return a *ConfigError; it returns a private wrapper type that contains the *ConfigError. My type assertion is looking for the wrapper to be the error, but the error is actually buried inside it.
Peeling back the layers with As
This is why we have to stop using type assertions for errors. I had to learn this the hard way after a few production bugs. The errors.As function is designed specifically for this "nested" scenario. It recursively unwraps the error chain until it finds a match for the type you're looking for.
Let's adjust the main function:
func main() {
err := MainProcess()
var cfgErr *ConfigError
if errors.As(err, &cfgErr) {
fmt.Println("Found the line number:", cfgErr.Line)
} else {
fmt.Println("Still no luck.")
}
}
Now it works. errors.As sees the wrapper, asks "Are you a *ConfigError?", gets a "no", then asks the wrapped error "Are you a *ConfigError?", and finally gets a "yes". It then populates the cfgErr variable with the pointer it found.
Adding a layer of intentionality
One last thing. Sometimes you don't just want to know the type of the error, but whether it matches a specific category of error, regardless of the specific instance. This is where errors.Is comes in, but if you want your custom type to be "is-able" without matching a specific instance, you can implement an Is(target error) bool method on your struct.
For example, maybe I want all ConfigError types to be considered "Temporary" if the line is 0 (meaning a generic IO issue). I can add this to my struct:
func (e *ConfigError) Is(target error) bool {
t, ok := target.(*ConfigError)
if !ok {
return false
}
// If the target is a "generic" ConfigError (line 0),
// and this error is also generic, they match.
return e.Line == 0 && t.Line == 0
}
By doing this, I've moved the logic of "what defines this error" inside the error type itself, rather than leaking that logic into my if/else blocks in main. It makes the error type a first-class citizen that knows how to describe its own identity.
📋 Practical Task
Implementing a Detailed API Validation Error Handler
You are building a user registration system. You need to create a custom error type called ValidationError that tracks which specific field failed (e.g., "email", "password") and the reason for the failure.
Your requirements:
- Create a
ValidationErrorstruct withFieldandReasonstrings. - Implement the
Error()method for this struct. - Create a function
ValidateUser(email string) errorthat returns a wrappedValidationErrorif the email is empty. - In your
mainfunction, callValidateUser(""). - Use
errors.Asto extract theValidationErrorand print a message specifically identifying which field caused the problem.
There are no comments for now.