04open 25 min

Error handling: no exceptions as control flow

Replace exception-driven flow with explicit error values and predictable control paths.

by the end of this lesson you can

  • Returns an error rather than a boolean-only success flag
  • Handles validation failure immediately
  • Keeps the happy path linear after the error checks

Overview

This is the first difference many Python developers feel in their hands. Go surfaces failure directly in the return value and expects you to deal with it immediately.

In Python, you often

let failures bubble up through exceptions and catch them where recovery makes sense, or sometimes not at all.

In Go, the common pattern is

to return `(value, err)` and handle the unhappy path right beside the call site. The repetition is deliberate: it makes failure visible.

why this difference matters

Once the pattern clicks, Go error handling feels less like ceremony and more like a habit that keeps surprising control flow out of the codebase.

Python

try:
    data = load_profile(user_id)
except ProfileError as exc:
    logger.error("profile failed: %s", exc)

Go

data, err := loadProfile(userID)
if err != nil {
    log.Printf("profile failed: %v", err)
    return err
}

Deeper comparison

Python version

def render_dashboard(user_id):
    try:
        profile = load_profile(user_id)
        stats = load_stats(user_id)
    except ProfileError as exc:
        logger.warning("profile unavailable: %s", exc)
        return None
    return build_dashboard(profile, stats)

Go version

func renderDashboard(userID int) (*Dashboard, error) {
    profile, err := loadProfile(userID)
    if err != nil {
        log.Printf("profile unavailable: %v", err)
        return nil, err
    }

    stats, err := loadStats(userID)
    if err != nil {
        return nil, err
    }

    dashboard := buildDashboard(profile, stats)
    return &dashboard, nil
}

Reflect

How does Go change your sense of where failure lives in a function compared with Python exceptions?

what a strong answer notices

A strong answer points out that the failure path is visible after each operation instead of being deferred to an outer try/except block.

Rewrite

Rewrite this exception-based Python control flow into Go using explicit error returns.

Rewrite this Python

def save_user(user):
    try:
        validate(user)
        repository.save(user)
    except ValidationError:
        return False
    return True

what good looks like

  • Returns an error rather than a boolean-only success flag
  • Handles validation failure immediately
  • Keeps the happy path linear after the error checks

Practice

Sketch a Go function that loads configuration, opens a database connection, and returns early if either step fails. Describe where you would log and where you would return the error.

success criteria

  • Shows two separate error checks rather than one generic catch-all
  • Returns the original error to the caller
  • Uses logging only where it adds context rather than hiding the failure

Common mistakes

  • Trying to hide repeated error checks inside abstractions too early.
  • Using panic where an error return is the normal control path.
  • Logging every error at every layer and making failures noisy rather than informative.

takeaways

  • Once the pattern clicks, Go error handling feels less like ceremony and more like a habit that keeps surprising control flow out of the codebase.
  • A strong answer points out that the failure path is visible after each operation instead of being deferred to an outer try/except block.
  • Shows two separate error checks rather than one generic catch-all