04open 25 min

Explicit errors: from thrown failures to visible control flow

Translate JavaScript and Node error instincts into Go's explicit error-return style.

by the end of this lesson you can

  • Returns an error explicitly
  • Handles validation failure without hiding control flow
  • Keeps the happy path linear after the checks

Overview

JavaScript developers are used to thrown errors, rejected Promises, and framework-specific error shapes. Go puts failure directly into return values and expects you to deal with it close to the call site.

In JavaScript/Node, you often

let failures reject Promises or throw errors that higher levels handle later.

In Go, the common pattern is

to return `(value, err)` and make the failure path visible immediately.

why this difference matters

This changes how you read code: failure stops being implicit background control flow and becomes part of the ordinary path through the function.

JavaScript/Node

try {
  const user = await loadUser(id);
} catch (error) {
  console.error(error);
}

Go

user, err := loadUser(id)
if err != nil {
    log.Println(err)
    return err
}

Deeper comparison

JavaScript/Node version

async function renderDashboard(id) {
  try {
    const profile = await loadProfile(id);
    const stats = await loadStats(id);
    return buildDashboard(profile, stats);
  } catch (error) {
    logger.warn(error);
    return null;
  }
}

Go version

func renderDashboard(id int) (*Dashboard, error) {
    profile, err := loadProfile(id)
    if err != nil {
        log.Println(err)
        return nil, err
    }

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

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

Reflect

What changes when you stop assuming failures travel outward invisibly and instead make them part of each function's visible flow?

what a strong answer notices

A strong answer mentions local clarity, easier debugging of unhappy paths, and fewer hidden control-flow jumps.

Rewrite

Rewrite this JavaScript error flow into Go using explicit error returns.

Rewrite this JavaScript/Node

function saveUser(user) {
  if (!user.name) {
    throw new Error("missing name");
  }
  return repository.save(user);
}

what good looks like

  • Returns an error explicitly
  • Handles validation failure without hiding control flow
  • Keeps the happy path linear after the checks

Practice

Design a Go function that loads config, opens a connection, and returns early if either step fails.

success criteria

  • Uses separate error checks for separate operations
  • Returns the original error clearly
  • Avoids trying to recreate Promise-style failure flow

Common mistakes

  • Trying to hide all repeated error checks immediately.
  • Expecting thrown-style control flow instead of visible returns.
  • Logging at every layer because JavaScript frameworks often centralize error shapes differently.

takeaways

  • This changes how you read code: failure stops being implicit background control flow and becomes part of the ordinary path through the function.
  • A strong answer mentions local clarity, easier debugging of unhappy paths, and fewer hidden control-flow jumps.
  • Uses separate error checks for separate operations