Error handling: explicit error returns without Result
Translate Result and Option habits into Go's error, wrapping, and nil-based control flow.
by the end of this lesson you can
- →Uses visible if err != nil checks
- →Keeps the happy path linear
- →Adds wrapping only where it improves context
Overview
Rust developers already think carefully about failure, but Go expresses that care differently. There is no Result type at the language level, and absence often shows up through nil, zero values, or an additional boolean. The skill is learning the control flow rather than searching for an exact type-level equivalent.
In Rust, you often
use Result, Option, and ? to make failure and absence explicit in the type system.
In Go, the common pattern is
to return error, handle it immediately, wrap it when context helps, and use nil or extra return values where absence is normal.
why this difference matters
Go still wants explicit error handling, but it encodes less of that structure in the type system itself.
Rust
let user = lookup_user(id).ok_or(UserError::Missing)?;Go
user, err := lookupUser(id)
if err != nil {
return nil, err
}
if user == nil {
return nil, ErrMissingUser
}Deeper comparison
Rust version
fn load(path: &str) -> Result<Config, ConfigError> {
let cfg = load_config(path)?;
Ok(cfg)
}Go version
func Load(path string) (Config, error) {
cfg, err := loadConfig(path)
if err != nil {
return Config{}, fmt.Errorf("load config: %w", err)
}
return cfg, nil
}Reflect
What changes when error handling stays explicit but the language gives you fewer tools to distinguish every case at the type level?
what a strong answer notices
A strong answer mentions local clarity, wrapping for context, and the need to choose clear API contracts without Result and Option everywhere.
Rewrite
Rewrite this Rust function into Go using explicit error returns.
Rewrite this Rust
fn save(user: User) -> Result<(), SaveError> {
validate(&user)?;
repo_save(user)?;
Ok(())
}what good looks like
- Uses visible if err != nil checks
- Keeps the happy path linear
- Adds wrapping only where it improves context
Practice
Design a Go function that loads config, validates it, and distinguishes ordinary absence from actual failure.
success criteria
- Uses error for failure
- Uses a separate contract for normal absence
- Explains the caller's control flow clearly
Common mistakes
- Looking for a direct Result replacement instead of learning Go's error style.
- Treating nil as automatically bad design rather than part of a deliberate API contract.
- Wrapping every error blindly instead of adding context where it matters.
takeaways
- ●Go still wants explicit error handling, but it encodes less of that structure in the type system itself.
- ●A strong answer mentions local clarity, wrapping for context, and the need to choose clear API contracts without Result and Option everywhere.
- ●Uses error for failure