1. Basic style for error messages
Error text is like a note that says “I’m out, the key is under the doormat.” You can write it clearly, or you can write it in a way that makes someone (possibly you, a week later) stand at the door thinking: “Doormat… which one of the twenty?!” In Go, an error often bubbles up through functions, accumulating context, and eventually surfaces in main almost in its original form: as a string someone once came up with.
In Go, error essentially has one main “public interface for humans” — the string returned by the Error() string method. Whatever you put there is what people will see in logs, in tests, in output. That’s why we don’t try to “guess” the reason by parsing the string, but we do want the string to be as useful as possible and easy to concatenate with context from other layers.
Now there will be a bit of “school grammar,” but it really saves hours of life. Go has a stable tradition: error messages are written in lowercase, without a period at the end, and without the "Error:" prefix. It’s not because gophers are allergic to capital letters; it’s because errors are often chained: “operation: reason: reason of the reason…”. If each layer starts with "Error:" and ends with a period, the final string looks like a badly glued press release.
It’s important to catch the idea: an error is not a “message to the user in the UI” (those may use capitals, periods, and politeness), but a technical description of a problem that’s convenient to read in logs and convenient to extend at each abstraction level.
Let’s compare examples.
| Situation | Bad | Better (Go-style) |
|---|---|---|
| invalid age | |
|
| empty title | |
|
| failed to parse a number | |
|
Note: “better” does not mean “in English.” You can do the same in any language, but within one project it’s important to keep a consistent style. In many Go projects, errors are written in English because it’s easier to search in the source, and because some libraries will return English text anyway. But the rule of lowercase / no period / no "Error:" works in any language.
Why “no period” and “lowercase” is an engineering decision
These rules didn’t appear “out of thin air.” In Go, it’s common to add context to an error while keeping the original reason text inside the message. A typical template looks like fmt.Errorf("decompress %v: %v", name, err) — i.e., on the left you add “what you were trying to do,” and on the right you glue on the original reason via a colon.
If the lower level returns "Invalid input." with a period, and the upper level adds context, you get something like:
"parse config: Invalid input.: ..."
A period in the middle looks strange. And if each level starts with "Error:", you get:
"Error: parse config: Error: invalid input"
In Go, it’s preferred that an error reads like a single phrase that grows left to right, so it’s convenient when it starts with a lowercase letter and doesn’t end with a period.
To solidify this, imagine error “growth” as a small diagram:
flowchart LR
A["low level: invalid syntax"] --> B["mid level: parse id: invalid syntax"]
B --> C["top level: mark done: parse id: invalid syntax"]
The idea is simple: each level adds operation context, rather than rewriting a novel titled “How I ended up here and how I feel.”
The “operation: reason” template is the main cheat code for good errors
When you write an error, it helps to think not “what’s wrong?”, but “which operation failed and why?”. Then the message becomes structured on its own, even if it’s just a string.
A very typical format:
- "operation: reason"
- sometimes a bit richer: "operation arg=value: reason"
Examples (no fanaticism, one line each):
- "parse age "4x": invalid syntax"
- "add task: title is empty"
- "mark done: task 17 not found"
This is especially important when you return an error “outward” and the calling code just prints it. Like in typical server/infrastructure code: the error is returned upward, and at the boundary it’s turned into text via err.Error() (or they just print err).
Quick self-check for an error message
Sometimes it’s useful to quickly run the message through a checklist to catch “cosmetic” things that later unexpectedly become a problem in logs and tests.
| Question about the message | If the answer is “no” — what to fix |
|---|---|
| Does it start with a lowercase letter? | Remove the capital letter at the beginning |
| No period at the end? | Remove the final period/exclamation mark |
| No "Error:" / "Error:"? | Remove the prefix; the error type already said it |
| Is there operation context? | Add "operation: ..." at the right level |
| Is the problematic value shown? | Add %q/%d if it’s safe |
Yes, it sounds like a style guide, but in Go this is really part of how code is commonly read and maintained.
2. What details to add to an error
Details are needed only as long as they answer the question: “what exactly broke?”. A good balance is usually: action + problematic value.
Strings are better shown via %q
With %s there’s an unpleasant problem: spaces and emptiness may not be visible. But %q shows the string in quotes and with escaping — so you immediately see what was really there.
package main
import (
"fmt"
"strconv"
)
func main() {
input := " "
_, err := strconv.Atoi(input)
if err != nil {
fmt.Printf("parse int %q: %v\n", input, err)
// parse int " ": invalid syntax
}
}
The style matters here: "parse int %q: ..." — lowercase, no period, no "Error:", and the input string is shown in a way that doesn’t let spaces “disappear.”
Add numbers and identifiers carefully
If you have a number that “broke” a validation, it’s worth including it. But don’t turn the error into a dump of the entire program state.
package main
import "fmt"
func ValidateLimit(limit int) error {
if limit < 1 {
return fmt.Errorf("limit must be >= 1, got %d", limit)
}
return nil
}
func main() {}
Note: it’s still one line and one meaning.
3. Mini example: a mini task list and Go-style errors
Let’s continue a learning mini project: a console program that keeps tasks in memory. We don’t use structs (we haven’t covered them yet), so we’ll store data with the simplest types possible: map[int]string for the title and map[int]bool for the status.
Task title validation: short and to the point
The first source of errors in programs like this is bad input. The most basic check: you can’t add a task with an empty title.
package main
import (
"errors"
"strings"
)
func validateTitle(title string) error {
if strings.TrimSpace(title) == "" {
return errors.New("title is empty")
}
return nil
}
Here the message is short, lowercase, without a period — and it doesn’t try to be “pretty.” It tries to be useful.
Now let’s add operation context at the call site so the final message shows what exactly we were trying to do:
package main
import "fmt"
func addTask(title string) (int, error) {
if err := validateTitle(title); err != nil {
return 0, fmt.Errorf("add task: %v", err)
}
return 1, nil
}
The trick: validateTitle() knows nothing about “adding a task”; it just validates a string. But addTask() knows the context — and adds it via "add task: ...". This approach naturally matches the Go practice of “adding information to an error via fmt.Errorf().”
Parsing a task id: make the error understandable
Let the "done" command take a task id as a string and turn it into an int. A parsing error is a separate story, and it’s worth showing it in a way that exposes the input value.
package main
import (
"fmt"
"strconv"
)
func parseID(s string) (int, error) {
id, err := strconv.Atoi(s)
if err != nil {
return 0, fmt.Errorf("parse id %q: %v", s, err)
}
if id < 1 {
return 0, fmt.Errorf("id must be >= 1, got %d", id)
}
return id, nil
}
Notice how the message will “glue together” if we add more context above. In the end you’ll get something like "mark done: parse id "x": invalid syntax" — it reads fine and without periods.
“Not found” is not a “format error,” but it’s still an error
When a task is not found, you may want to write something like "Error: Task not found!". Don’t. A technical phrasing is enough.
package main
import "fmt"
func markDone(titles map[int]string, done map[int]bool, id int) error {
if _, ok := titles[id]; !ok {
return fmt.Errorf("task %d not found", id)
}
done[id] = true
return nil
}
Again, the message is lowercase, without a period. If we want, we can add “operation” at a higher level so the error becomes even clearer, without changing markDone():
package main
import "fmt"
func handleDone(titles map[int]string, done map[int]bool, idStr string) error {
id, err := parseID(idStr)
if err != nil {
return fmt.Errorf("done: %v", err)
}
if err := markDone(titles, done, id); err != nil {
return fmt.Errorf("done: %v", err)
}
return nil
}
An important nuance: "done: %v" repeats, and that’s fine for a learning example. In a real project, you’d be a bit more careful about where exactly you add context so you don’t end up with "done: done: ...", but today the principle matters more than perfect factoring.
4. Common mistakes
Mistake #1: "Error:" at the beginning of the message.
Often this comes from other languages or from the habit of writing messages “as if for a user.” In Go it’s extra: you already have the error type and the if err != nil branch, so the prefix only clutters the string and makes error chains ugly and repetitive.
Mistake #2: a period at the end and capital letters “by the rules.”
The paradox is that in error chains, “correct punctuation” starts working against you. As soon as you add context via fmt.Errorf("operation: %v", err), the period ends up in the middle of the string. That’s why it’s customary to write errors in lowercase and without a period: they compose better.
Mistake #3: a message that’s too generic and has no context ("invalid input").
Such a message is useless because it doesn’t answer “which input and in which operation.” The fix is usually simple: add operation context and the value, for example "parse id %q: ..." or "limit must be >= 1, got %d".
Mistake #4: a “three-volume novel” instead of one line.
Sometimes you want to explain everything at once: what the program expected, what it got, what the user should do, and also apologize. In a technical error, this hurts readability. Better: one line, one meaning, plus the value that broke validation.
Mistake #5: a message that can’t be safely chained with others.
If you write errors like "Invalid something!" and then add "do X: …" above, the result can become awkward: "do X: Invalid something!" plus even more context. Go style is about letting errors be extended calmly as they’re returned higher up the call stack.
GO TO FULL VERSION