The one mental switch that unlocks reading every Go PR. No exceptions — literally.
if err != nil { return ... }
appears in nearly every function you'll review. It's also the #1 thing Go reviewers flag.
Master this and you can parse the control flow of any Go diff.
In Python, failure travels up the stack on its own. You call a function and trust that if something breaks, an exception will unwind until someone catches it. The happy path reads top-to-bottom; errors are an out-of-band channel.
Go deletes that channel. There is no throw, no raise, no exceptions for ordinary failure.
A function that can fail simply returns the error as its last value, and the caller must look at it. This is the
error-as-value model.
def get_dapp(id):
# raises KeyError if missing
return dapps[id]
# caller
try:
d = get_dapp(id)
except KeyError:
d = None
func GetDApp(id string) (*DApp, error) {
d, ok := dapps[id]
if !ok {
return nil, ErrNotFound
}
return d, nil
}
// caller — must inspect err
d, err := GetDApp(id)
if err != nil { /* handle */ }
An error is just an interface with one method (you'll meet interfaces properly in a later lesson):
type error interface {
Error() string
}
So a function returning error is returning a value you can store, compare, wrap, or ignore — it does not
hijack control flow. [Go blog: Error handling and Go]
Because errors are values, the caller checks immediately and returns early. This is the early-return / indent-error-flow idiom. The official rule:
if body ends in break, continue, goto, or
return, the unnecessary else is omitted — the happy path stays at the leftmost indentation.
[source]
d, err := GetDApp(id)
if err == nil {
use(d) // happy path
return d, nil // drifts right
} else {
return nil, err
}
d, err := GetDApp(id)
if err != nil {
return nil, err // bail first
}
use(d) // happy path, flat
return d, nil
Reading trick: every if err != nil block is a place the function can exit. Scan a Go function by
jumping between these blocks — they are the failure map. Whatever's left, un-indented, is the success story.
From portfolio-service-go/services/dapp/service.go — a service you'll review.
func (s *Service) GetCachedByID(ctx context.Context, id string) (*models.DApp, error) {
dApps, _, err := s.cache.dApps.Get(ctx)
if err != nil {
return nil, err // ① bail on infra error
}
if dApp, ok := dApps[id]; ok { // ② comma-ok map read
return dApp, nil // ③ happy path
}
return nil, ErrNotFound // ④ sentinel: "not found" is not a crash
}
var ErrNotFound = errors.New("not found")
Read it as a reviewer — four exit points, three concepts:
err up.dApps[id] on a missing key does not raise — it returns the
zero value and ok == false. The Python KeyError has no equivalent here.ErrNotFound — a normal value the caller can test with errors.Is(err, ErrNotFound).v := m[k] and then uses v without the ok check,
that may be a bug hiding behind a zero value, not a crash. That's a real review comment to leave.
_ and discarded. Usually wrong. [rule]Capitalized or end in punctuation — they get concatenated into bigger messages, so they must be lowercase, no trailing period. [rule]if err == nil { ... } instead of early return — harder to read, drifts right.Pick the answer. Feedback is instant. No score kept — this is for the rep, not the grade.
GetCachedByID above, how many ways can the function return to its caller?val, _ := cache.Get(ctx, key)
return process(val)err comes last, or what errors.Is / %w wrapping does, or how the
errorx namespaces in services/charge/errors.go relate to plain sentinels? Ask in chat. Next
lesson goes deeper into the error chain (errors.Is, errors.As, wrapping) using your repo's own patterns.
Now in your GLOSSARY.md:
error is a returned value, not a thrown exception.var Err... = errors.New(...) callers test with errors.Is.v, ok := m[k]; the bool reports presence. Missing key → no error.