04open 25 min

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