DEV Community

Cover image for Go Error Strings: Lowercase, No Punctuation, and Why It Matters
Gabriel Anhaia
Gabriel Anhaia

Posted on

Go Error Strings: Lowercase, No Punctuation, and Why It Matters


You open a log line during an incident and read this:

Failed to connect.: users.Load: Timed out.
Enter fullscreen mode Exit fullscreen mode

Three error strings, written by three people, glued together
by fmt.Errorf. One capital F, one trailing period jammed
against a colon, one more capital in the middle. It reads like
a ransom note. Nobody set out to produce that. It's what you
get when every layer capitalizes its own sentence and punctuates
its own ending, then the wrapping machinery staples them into
one line.

Go has a convention that prevents this exact mess. Error strings
start lowercase and carry no trailing punctuation. It is written
down in the official Go error string
guidance
, enforced by a
staticcheck rule, and the reason it exists is mechanical.

The convention, stated plainly

An error string in Go should:

  • start with a lowercase letter
  • have no trailing punctuation (no ., no !)

So this is wrong:

return errors.New("Failed to open file.")
Enter fullscreen mode Exit fullscreen mode

And this is right:

return errors.New("failed to open file")
Enter fullscreen mode Exit fullscreen mode

The exception is when the first word is a proper noun,
an acronym, or an identifier that is always capitalized.
HTTP, TCP, JSON, an exported type name, a URL. Those keep
their case because lowercasing them would be wrong on their own
terms:

return errors.New("HTTP response missing body")
Enter fullscreen mode Exit fullscreen mode

That's the whole rule. The interesting part is why it exists.

Errors wrap into sentences, and you don't own the whole sentence

The reason is the wrapping. An error in Go rarely travels alone.
It gets caught, annotated with context, and passed up. The
standard way to annotate is fmt.Errorf with the %w verb:

func Load(id string) (*User, error) {
    row, err := db.Query(id)
    if err != nil {
        return nil, fmt.Errorf("users.Load %s: %w", id, err)
    }
    return row, nil
}
Enter fullscreen mode Exit fullscreen mode

The caller's Errorf prepends its own context and wraps the
error underneath. If the bottom error was connection refused
and the query layer wrapped it, and the handler wrapped that,
the final string is one continuous line:

handler: users.Load 42: connection refused
Enter fullscreen mode Exit fullscreen mode

That reads cleanly because every fragment is lowercase and
none of them terminate. You are never writing a complete
sentence. You are writing one clause that might sit in the
middle of a chain someone else assembles at runtime. A capital
letter in the middle of that chain looks like a typo. A period
in the middle cuts the sentence in half:

handler: users.Load 42: Connection refused.
Enter fullscreen mode Exit fullscreen mode

The convention is what makes the fragments compose. Lowercase
so the fragment can start anywhere. No trailing period so the
next fragment can follow it.

The one place capital-and-period is fine

The rule is about wrapped errors, the kind that flow through
%w and end up mid-sentence. It does not apply to text that is
always the last thing a human reads and never gets wrapped: a
top-level message printed to the terminal, or the argument to
log.Fatal at the edge of a program.

if err := run(); err != nil {
    fmt.Fprintln(os.Stderr, "Could not start server.")
    os.Exit(1)
}
Enter fullscreen mode Exit fullscreen mode

That string is a user-facing sentence, not an error value that
propagates. It can be capitalized and punctuated like normal
prose. The distinction is whether the string is a Go error
that other code will wrap, or final output that nothing will
build on. Error values follow the convention. Terminal prose
does not have to.

The tools that enforce it

You do not have to police this by hand. Two checkers cover it.

go vet catches the mechanical Errorf mistakes: a %w with
no matching error, a wrong verb, a bad format string. It does
not flag capitalization, but it keeps your wrapping honest so
the chain actually forms.

$ go vet ./...
# example
./users.go:14: fmt.Errorf format %w has arg id of wrong type string
Enter fullscreen mode Exit fullscreen mode

The capitalization and punctuation rule lives in
staticcheck, under the check
ST1005, "error strings should not be capitalized." Run it
across the module:

$ staticcheck ./...
users.go:14:25: error strings should not be
    capitalized (ST1005)
Enter fullscreen mode Exit fullscreen mode

ST1005 also flags trailing newlines and, in recent versions,
error strings that end in punctuation. If you use
golangci-lint, the stylecheck linter carries the same
ST1005 rule; enable it and the whole team gets the check in
CI without anyone remembering the convention.

# .golangci.yml
linters:
  enable:
    - stylecheck
Enter fullscreen mode Exit fullscreen mode

One check turns "please write errors lowercase" from a code
review comment you make forever into a build failure nobody
argues with.

Formatting a chain cleanly

Enforcement stops you writing bad fragments. It does not tell
you how to write good ones. A few habits make the assembled
chain readable.

Name the operation, not the outcome. The wrapping layer
already tells the reader something failed, because it is an
error. Your fragment should say what you were doing, so the
final chain reads like a path:

// noise: every layer repeats "failed"
return fmt.Errorf("failed to load user: %w", err)

// better: each layer names its operation
return fmt.Errorf("load user %s: %w", id, err)
Enter fullscreen mode Exit fullscreen mode

Assembled, the second style gives you a trail you can follow
from the outside in:

serve request: load user 42: query users: connection refused
Enter fullscreen mode Exit fullscreen mode

Every colon is a layer boundary. You can read where the failure
started and every step it passed through on the way up.

Put %w last and let the colon do the joining. The Go
idiom is fmt.Errorf("<context>: %w", err). The : separator
between your context and the wrapped error is what makes the
chain scan as one line. Keep the context short. It is a label,
not a paragraph.

Wrap once per meaningful boundary, not once per function.
If a function just calls another and passes the error straight
up, wrapping it again adds a segment that says nothing:

// adds no information, just lengthens the chain
if err != nil {
    return fmt.Errorf("call helper: %w", err)
}
Enter fullscreen mode Exit fullscreen mode

Wrap where you add real context: an ID, a filename, the name of
the external system you were talking to. Skip it where you are
only a pipe.

Reach for errors.Is and errors.As, not string matching.
Because the convention keeps error text short and stable, it is
tempting to match on it. Don't. The text is for humans. For
control flow, wrap sentinels with %w and test them
structurally:

if errors.Is(err, sql.ErrNoRows) {
    return nil, ErrUserNotFound
}
Enter fullscreen mode Exit fullscreen mode

The string tells the on-call engineer what happened. errors.Is
tells the program what to do. Keeping those two jobs separate is
the reason the convention can stay small.

Why a formatting rule earns its keep

A lowercase-no-period rule sounds like bikeshedding. It is not,
because Go errors are values that compose at runtime, and
composition only works when the pieces agree on a shape. The
convention is the shared contract that lets a fragment written
in the database layer sit cleanly next to a fragment written in
the handler, in a chain neither author saw coming. go vet and
ST1005 make the contract automatic. The habits above make the
result readable. Together they turn error strings from a wall of
mismatched sentences into a single line you can act on at 3 a.m.

Grep your codebase for errors.New("[A-Z] and
fmt.Errorf("[A-Z]. Whatever comes back is a fragment that will
one day land in the middle of a chain wearing a capital letter.
Fix those, turn on ST1005, and you never have to think about
it again.


Error handling is one of those Go topics that looks trivial from
the outside and turns out to be the connective tissue of every
service you run. The Complete Guide to Go Programming goes deep
on how error, %w, errors.Is, and errors.As actually work
under the hood. Hexagonal Architecture in Go shows where to
wrap and where to translate errors so the convention holds at
your port boundaries instead of leaking framework noise inward.

Thinking in Go — the 2-book series on Go programming and hexagonal architecture

Top comments (0)