Error Message Style in Go and the Lowercase Convention
A reference for Go error message style: lowercase, no trailing punctuation, no failed to, name the operation and the value, with a table and a tiny checker.

Every Go program ends up printing error chains: start server: load config: open app.toml: no such file or directory. Whether such a line reads smoothly or like a pile of sentences depends on a handful of conventions that the standard library follows and the Go code review comments spell out. This page is the reference: the rules, a table of good and bad forms, and a small checker you can adapt. For the reasoning about wrapping itself, see adding context to errors and wrapping with %w.
Tested with Go 1.25.6, macOS on Apple Silicon (arm64)
It belongs to the error handling guide. Everything ran on Go 1.25.6.
The rules
1. Lower case, no trailing punctuation. An error string is usually printed after other text, and a capital letter or a full stop in the middle of a chain looks wrong. The Go code review comments put it this way: error strings should not be capitalised, unless they begin with proper nouns or acronyms, or end with punctuation, because they are usually printed following other context. The exception is a proper noun or acronym: HTTP status 502 and Redis connection lost are fine.
2. Say what you were doing. A wrapped error's prefix is a short phrase naming the operation: read config, charge card. A verb and a noun is plenty.
3. Include the value that distinguishes this call. parse port "80a" beats invalid port. Use %q for strings so empty strings and stray spaces show up: "" is visible, and an empty %s is not.
4. Leave out filler. "failed to", "error while", "unable to" and "error:" add nothing, since the reader knows it is an error and the chain already implies failure. The stdlib says open x: no such file, not failed to open x.
5. Don't repeat what the cause says. os.Open already includes the path. A wrapper that adds the path again stutters.
6. One line. Embedded newlines break log parsing and terminal output. Put multi-line detail in a joined error, which is deliberately multi-line, or in a structured log field.
7. Prefix sentinels with their package. Sentinel errors travel without a wrapper and may be printed alone, so store: key not found explains itself where not found would not. The standard library does this: io: read/write on closed pipe, http: server closed idle connection.
8. Keep secrets out. Do not format passwords, tokens or full request bodies into an error string, since error text ends up in logs and sometimes in responses.
9. Messages for people versus logs. A user-facing message is a sentence with a capital and a full stop; an error chain is for engineers. Keep them separate (user-facing and internal errors).
Good and bad
| Instead of | Write | Rule |
|---|---|---|
Failed to open file. | open config: %w | 1, 4 |
error: invalid port | parse port %q: %w | 3, 4 |
could not connect to database | connect to database %q: %w | 3 |
Something went wrong! | name the operation | 2 |
read /etc/app.toml: open /etc/app.toml: ... | read config: %w | 5 |
key not found (exported sentinel) | store: key not found | 7 |
bad value then a second line | bad value: expected int | 6 |
A tiny checker
The rules are mechanical enough to check in code, which is what linters do. Staticcheck's check ST1005 reports incorrectly formatted error strings, and golangci-lint can run it. The same idea in a few lines, for a quick experiment:
package main
import (
"fmt"
"strings"
"unicode"
)
func problems(msg string) []string {
var out []string
if msg == "" {
return []string{"empty"}
}
first := []rune(msg)[0]
if unicode.IsUpper(first) && !strings.HasPrefix(msg, "HTTP") {
out = append(out, "starts with a capital")
}
if strings.HasSuffix(msg, ".") || strings.HasSuffix(msg, "!") || strings.HasSuffix(msg, "?") {
out = append(out, "ends with punctuation")
}
for _, filler := range []string{"failed to", "unable to", "error:", "error while"} {
if strings.Contains(strings.ToLower(msg), filler) {
out = append(out, "contains "+`"`+filler+`"`)
}
}
if strings.Contains(msg, "\n") {
out = append(out, "has a newline")
}
return out
}
func main() {
for _, msg := range []string{
"load config: open app.toml: no such file or directory",
"Failed to open file.",
"error: invalid port",
"HTTP status 502",
"two lines\nof text",
} {
fmt.Printf("%-55q %v\n", msg, problems(msg))
}
}"load config: open app.toml: no such file or directory" []
"Failed to open file." [starts with a capital ends with punctuation contains "failed to"]
"error: invalid port" [contains "error:"]
"HTTP status 502" []
"two lines\nof text" [has a newline]It is deliberately naive: the capital-letter test would flag proper nouns it does not know about, and real linters work on the syntax tree and on fmt.Errorf calls. But it shows that the conventions are specific enough to automate, which is the reason to adopt them.
Messages that read well in a chain
The real test is how the whole chain reads. Compose three layers and read left to right:
handle checkout: charge card for order 8812: call gateway: Post "https://pay.example.com/v1/charge": context deadline exceededEach segment is lower case and names an action, the order number identifies the call, and the last segment is the cause. If the sentence reads like a path from intention to root cause, the style is working. If any segment makes you stumble, it probably breaks a rule above.
When the rules do not apply
Log messages are not error strings. A line such as slog.Error("request failed", "err", err) is a complete message on its own line, and the capitalisation rule is for strings that get embedded. Panics are in between: the panic value is usually a string or error and is printed by the runtime after panic: , so a lower-case phrase reads naturally. Messages shown to end users follow the style of your product copy, with capitals and full stops, and should not be produced by calling err.Error().
For the primary source of the main rule, see the Go code review comments on error strings, and for the conventions around values and wrapping, Effective Go.


