fmt.Errorf — Junior Level¶
Table of Contents¶
- Introduction
- Prerequisites
- Glossary
- Core Concepts
- Real-World Analogies
- Mental Models
- Pros & Cons
- Use Cases
- Code Examples
- Coding Patterns
- Clean Code
- Product Use / Feature
- Error Handling
- Security Considerations
- Performance Tips
- Best Practices
- Edge Cases & Pitfalls
- Common Mistakes
- Common Misconceptions
- Tricky Points
- Test
- Tricky Questions
- Cheat Sheet
- Self-Assessment Checklist
- Summary
- What You Can Build
- Further Reading
- Related Topics
- Diagrams & Visual Aids
Introduction¶
Focus: "How do I create an error with a formatted message?"
You already know errors.New("something failed") — a one-liner that builds a basic error from a fixed string. But real programs do not deal in fixed strings. They deal in this user, that file, the third byte in a sequence. You need an error that includes runtime values: an ID, a path, a port number, the original cause.
That is exactly what fmt.Errorf is for.
It looks like fmt.Sprintf, except the result is an error value, not a string. The verbs you know from Printf work the same: %s, %d, %v, %q, %t, and so on.
But there is one verb you have probably never seen anywhere else: %w. It is unique to fmt.Errorf. It does not format anything by itself — instead, it asks the function to wrap an existing error so the chain stays inspectable. This single verb is the most important thing in this whole file.
After reading this you will: - Know the signature fmt.Errorf(format string, a ...any) error. - Be able to format runtime values into an error message. - Understand why %w is different from %v and when to use each. - Recognize the typical idiom fmt.Errorf("op: %w", err) and read it correctly. - Avoid the most common mistakes: wrapping nil, using %w on non-errors, using %v and losing the chain.
Prerequisites¶
- Required: Functions and multiple return values — you already write functions returning
(T, error). - Required: Basic familiarity with the
errorinterface (covered in 5.2). - Required:
errors.New— the simplest way to make an error (covered in 5.3). - Required:
fmt.Printf/fmt.Sprintf— knowing what%v,%s,%d,%qdo. - Helpful: A glance at
errors.Isanderrors.As— you do not need to use them yet, just know they exist (full coverage in 5.5).
Glossary¶
| Term | Definition |
|---|---|
| fmt.Errorf | A standard-library function that formats values and returns an error, like Sprintf for errors. |
| format string | The first argument; contains literal text plus verbs like %s, %d, %w. |
| verb | A single placeholder in the format string. %v is generic, %s is string, %d is integer, %w is wrap. |
| wrap | To attach context to an error while keeping the original findable via errors.Is and errors.As. |
| embed | To paste the original error's text into the new error's message, throwing away the chain. |
| error chain | A linked list of errors created by repeated wrapping. The "outer" error remembers the "inner" one. |
| Unwrap | The method that returns the next error in the chain. errors.Is uses it under the hood. |
| %w verb | The wrap verb, valid only inside fmt.Errorf. Argument must be an error. |
| wrapError | The internal struct created when you call fmt.Errorf with a single %w. Implements Unwrap(). |
| wrapErrors | The internal struct created when you call fmt.Errorf with multiple %w (Go 1.20+). |
Core Concepts¶
Concept 1: fmt.Errorf builds an error from a format string¶
The signature is exactly the one you would expect:
You hand it a format string and some arguments; it returns an error whose Error() method gives you the formatted text.
err := fmt.Errorf("user %d not found in table %q", id, "users")
fmt.Println(err) // user 42 not found in table "users"
If you have ever called fmt.Sprintf, you already know how to call fmt.Errorf. The only difference is the return type: error instead of string.
Concept 2: All the usual verbs work¶
Anything you would put in a Sprintf call works here too:
| Verb | Meaning | Example |
|---|---|---|
%v | Default format for any value | 42, [1 2], {Alice 30} |
%s | String value (or Error() for errors) | "hello" |
%d | Decimal integer | 42 |
%q | Quoted string | "hello" |
%t | Boolean | true |
%x | Hex | 2a |
%T | Type | *main.User |
%w | Wrap an error | (special — see below) |
Concept 3: %w wraps; %v only formats¶
This is the one concept that makes fmt.Errorf different from fmt.Sprintf. When you have an existing error and you want to add context to it, you have two choices:
Embedding (loses the chain):
The originalerr is rendered into a string and embedded into the new error. After this, errors.Is(newErr, originalErr) is false. The new error is just text. Wrapping (preserves the chain):
Now the resulting error has an internalUnwrap() error method that returns the original err. errors.Is(newErr, originalErr) is true. The new error's Error() text looks the same, but its identity is preserved. Rule of thumb: if you are wrapping an error to add context for callers, use %w. Use %v only when the original error is genuinely just diagnostic text, not something a caller might want to inspect.
Concept 4: One %w per call (or many in Go 1.20+)¶
Before Go 1.20, you could only use %w once in a single fmt.Errorf call. Using it twice was a runtime mistake — the second one came out as %!w(...).
Since Go 1.20:
This wraps both errA and errB. errors.Is(err, errA) is true and errors.Is(err, errB) is true. Useful for "the operation had two simultaneous failures" cases.
You will see single-%w 99% of the time.
Concept 5: The %w argument must be an error¶
%w requires the corresponding argument to satisfy the error interface. If it does not, the formatted output contains %!w(<type>=<value>) to mark the misuse — and the wrapping does not happen.
This is a silent runtime bug, not a compile error. Always pass an actual error to %w.
Real-World Analogies¶
| Concept | Analogy |
|---|---|
| Format verbs | Filling in blanks in a paragraph: "User ___ failed at step ___." Each blank has a type. |
%v (embed) | Photocopying a receipt and stapling it inside another report. The original is now a copy of text — the receipt itself is gone. |
%w (wrap) | Stapling the original receipt to the report. The new report references the receipt, but the receipt is still its own thing. |
Multiple %w | Stapling two receipts to one report — the report says "I had two attached costs." |
%w with a non-error | Asking for a stapled "receipt" but handing the stapler a coffee cup. The system shrugs and writes "type mismatch" on the report. |
Mental Models¶
The intuition: fmt.Errorf is fmt.Sprintf with one extra trick. The trick is %w, which says "do not just print this error — remember it." Everything else is regular formatting.
Why this model helps: It kills the temptation to think of %w as a magic verb. It is just a directive that flips a bit on the resulting error: "I have a wrapped child." That bit is what errors.Is and errors.As use to walk the chain. No magic, just a hidden field.
Second intuition: Think of %w as "I am storing a pointer," and %v as "I am stamping a string." After %v, only text remains. After %w, a pointer remains and the text is a side effect for human eyes.
Third intuition: Each call to fmt.Errorf("...: %w", err) adds one link to a chain. The deeper your call stack, the longer the chain. Reading the chain top-to-bottom is reading "what was the operation, then what went wrong, then what really went wrong."
Pros & Cons¶
| Pros | Cons |
|---|---|
| One function for both formatting and wrapping. | The %w vs %v distinction is invisible at the call site if you do not look closely. |
Output text is identical for %w and %v — only identity differs, so swapping accidentally is silent. | Minor cost: 1–3 allocations per call vs 0 for a package-level sentinel. |
Plays well with the rest of the errors package: Is, As, Unwrap. | Verbose for one-off "thing happened" errors — errors.New is shorter. |
| Standard, idiomatic, no third-party dependency required. | Pre-Go 1.20: only one %w per call; the rule was easy to forget. |
| Multi-wrap (Go 1.20+) collects multiple causes cleanly. | Wrapping a nil produces a non-nil result that prints %!w(<nil>) — caller bugs. |
When to use:¶
- Whenever you propagate an error and want to add context (operation name, ID, path, etc.).
- Whenever you want callers to still find the underlying error via
errors.Is/errors.As.
When NOT to use:¶
- When the message is fully static and never includes runtime values — use
errors.Newinstead. - When the error is a top-level package sentinel (
var ErrFoo = errors.New("foo")).
Use Cases¶
- Adding context to a propagated error:
fmt.Errorf("save user %d: %w", id, err). - Building a sentinel-bearing error:
fmt.Errorf("lookup %q: %w", key, ErrNotFound). - Wrapping a typed error from a stdlib call:
fmt.Errorf("connect %s: %w", addr, sysErr). - Reporting multiple simultaneous failures (Go 1.20+):
fmt.Errorf("commit: %w; rollback: %w", commitErr, rollbackErr). - Producing a structured-looking message with formatted runtime data:
fmt.Errorf("port %d out of range [%d,%d]", p, lo, hi).
Code Examples¶
Example 1: Basic formatting¶
package main
import "fmt"
func main() {
id := 42
err := fmt.Errorf("user %d not found", id)
fmt.Println(err) // user 42 not found
fmt.Println(err.Error()) // same string
}
What it does: fmt.Errorf returns an error whose textual form contains the formatted message. err.Error() and fmt.Println(err) produce the same output.
Example 2: Wrapping with %w¶
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("not found")
func lookup(id int) error {
return fmt.Errorf("lookup %d: %w", id, ErrNotFound)
}
func main() {
err := lookup(7)
fmt.Println(err) // lookup 7: not found
fmt.Println(errors.Is(err, ErrNotFound)) // true
}
What it does: Wraps ErrNotFound so the caller can still detect it with errors.Is. The printed text shows both the context and the original message. How to run: go run main.go.
Example 3: Embedding with %v (the wrong choice in most cases)¶
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("not found")
func lookupBad(id int) error {
return fmt.Errorf("lookup %d: %v", id, ErrNotFound) // note %v
}
func main() {
err := lookupBad(7)
fmt.Println(err) // lookup 7: not found
fmt.Println(errors.Is(err, ErrNotFound)) // false!
}
What it does: Looks identical when printed but errors.Is cannot find the sentinel. The text matches; the identity does not.
Example 4: Wrapping a stdlib error¶
package main
import (
"errors"
"fmt"
"io/fs"
"os"
)
func readConfig(path string) ([]byte, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read config %q: %w", path, err)
}
return data, nil
}
func main() {
_, err := readConfig("/no/such/file.json")
fmt.Println(err)
fmt.Println(errors.Is(err, fs.ErrNotExist)) // true
}
What it does: os.ReadFile returns an error wrapping fs.ErrNotExist. Wrapping that error again with %w keeps the chain intact, so errors.Is(err, fs.ErrNotExist) still works at the top.
Example 5: Multiple verbs, including %w¶
package main
import (
"errors"
"fmt"
)
func main() {
cause := errors.New("permission denied")
err := fmt.Errorf("user=%q action=%q port=%d: %w",
"alice", "open", 22, cause)
fmt.Println(err)
// user="alice" action="open" port=22: permission denied
}
What it does: Demonstrates that %w peacefully co-exists with other verbs. Only one verb %w per call (pre-1.20) but unlimited %v/%s/%d.
Example 6: Multi-wrap (Go 1.20+)¶
package main
import (
"errors"
"fmt"
)
var (
ErrA = errors.New("A failed")
ErrB = errors.New("B failed")
)
func main() {
err := fmt.Errorf("multi: %w; %w", ErrA, ErrB)
fmt.Println(err) // multi: A failed; B failed
fmt.Println(errors.Is(err, ErrA)) // true
fmt.Println(errors.Is(err, ErrB)) // true
}
What it does: Both ErrA and ErrB are wrapped. errors.Is finds either one. The string just shows them concatenated.
Every example here is runnable. Save with
package mainandfunc main().
Coding Patterns¶
Pattern 1: Operation prefix + %w¶
The format "<operation>: %w" is the most common shape in real Go code. Read top-to-bottom, the chain reads like a stack of operations.
Pattern 2: Sentinel-bearing error¶
var ErrNotFound = errors.New("not found")
func get(id int) error {
return fmt.Errorf("get %d: %w", id, ErrNotFound)
}
The caller does errors.Is(err, ErrNotFound). Wrap the sentinel so context is preserved without losing identity.
Pattern 3: Validate and report¶
No wrap needed — there is no underlying error. Just formatted runtime data.
Pattern 4: Wrap then re-wrap¶
func loadConfig(path string) error {
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("read %q: %w", path, err)
}
if err := parse(data); err != nil {
return fmt.Errorf("parse %q: %w", path, err)
}
return nil
}
Each layer adds context. The final printed error reads like a sentence.
Pattern 5: Quoted string verb %q¶
%q adds quotes around a string, useful for visually separating the name from the surrounding text.
Clean Code¶
- Put the operation name first, then the values, then
%w."save user 42: db down"is easier to read than"db down while saving user 42". - Lowercase first letter, no trailing period:
"open file: %w", not"Open file. %w". - Use
%qaround strings that could be empty or contain spaces — it makes the boundary visible:read %qshowsread ""for an empty path. - One
%wperfmt.Errorffor clarity. Multi-wrap is a feature, not a default. - If the message is a fixed string, use
errors.New, notfmt.Errorf.fmt.Errorf("oops")is heavier thanerrors.New("oops").
Product Use / Feature¶
A signup handler:
func handleSignup(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
log.Print(fmt.Errorf("signup read body: %w", err))
http.Error(w, "bad request", 400)
return
}
var req SignupRequest
if err := json.Unmarshal(body, &req); err != nil {
log.Print(fmt.Errorf("signup parse JSON: %w", err))
http.Error(w, "invalid JSON", 400)
return
}
if err := userService.Create(r.Context(), req); err != nil {
log.Print(fmt.Errorf("signup create user %q: %w", req.Email, err))
http.Error(w, "internal error", 500)
return
}
w.WriteHeader(http.StatusCreated)
}
Each fmt.Errorf annotates the failure with the step that produced it. The wrapped errors preserve the original cause for log filtering and debugging.
Error Handling¶
Yes, this is error handling, but here are sub-rules specific to fmt.Errorf:
- Use
%wwhenever you might want callers to inspect the wrapped error. - Do not wrap something and then return only
err.Error(). You lose the chain. - Do not call
fmt.Errorfon anilerror — wrapping nil produces an error whose text is"... %!w(<nil>)"and the chain points to nothing meaningful. - Always check the original error first, then wrap on the failure path.
Security Considerations¶
- Do not interpolate secrets.
fmt.Errorf("auth failed for token %q", token)writes the token into log files forever. Mask tokens, hashes, passwords. - Do not echo user input verbatim in errors that propagate to UIs — sanitize or quote with
%qand consider truncation. - Be careful with
%vof structs. A struct containingPassword stringwill helpfully print its password. Either implement a redactedString()method or do not pass the whole struct.
Performance Tips¶
fmt.Errorfallocates 1 to 3 times per call (formatted string + wrapper struct).- For static messages, prefer
errors.New(one allocation) or a package-level sentinel (zero per call). - Wrapping is cheap in absolute terms (~150 ns) but multiplies in tight loops. For hot paths, wrap once at the boundary.
- See
optimize.mdfor a full breakdown.
Best Practices¶
- Wrap with
%w, not%v. Default to wrapping unless you have a reason to flatten. - Add context, not noise. "save user 42: db down" is good. "an error occurred during operation: db down" is wrap-for-the-sake-of-wrap.
- Lowercase, no period. Standard Go convention.
- Use
%qfor strings. Especially user-supplied paths and identifiers. - One
%wper call unless you specifically want multi-wrap semantics. - Never wrap nil. Always check
if err != nilfirst.
Edge Cases & Pitfalls¶
fmt.Errorf("%w", nil)— produces a non-nil error with text"%!w(<nil>)". Looks like a wrap but unwraps to nothing useful.%wwith a non-error argument — produces"%!w(<type>=<value>)". Silent runtime bug.- More than one
%wbefore Go 1.20 — only the first wraps; the rest become%!w(...). %wand%vtogether for the same error — wrap once, do not also embed:- Calling
fmt.ErrorfwithErrorfaliases. Some logging libraries accept format strings shaped likeErrorf's but do not implement%w. Read the doc.
Common Mistakes¶
- Using
%vinstead of%wwhen you intended to wrap. Output looks identical;errors.Isfails. - Wrapping nil by forgetting the
if err != nilcheck. - Passing a non-error to
%w— string, struct, ornilinterface. - Capitalizing the message:
fmt.Errorf("Failed to read")— should be lowercase. - Ending with a period:
fmt.Errorf("read failed.")— Go errors are sentence fragments. - Using
fmt.Errorfinstead oferrors.Newfor static messages. - Re-wrapping the same error twice in one call:
fmt.Errorf("%w: %w", err, err). - Hard-coding the wrapped error's text into the format:
fmt.Errorf("read failed: %s", err.Error())— flatten and you have to callError()manually.
Common Misconceptions¶
- "
%wis a string verb." No — it does not produce text directly. It wraps; the text is a side effect becauseErrorfalso formats the wrapped error'sError()into the output. - "
%walways panics if argument is not an error." No — it produces%!w(...)and continues. The bug is silent. - "
fmt.Errorfanderrors.Neware interchangeable." No —fmt.Errorfdoes formatting (and optionally wrapping);errors.Newdoes neither. Pick the lighter one when there is no formatting to do. - "Multi-wrap is always available." Only since Go 1.20. Earlier versions limit you to one
%w.
Tricky Points¶
%wonly insidefmt.Errorf. It is not a general format verb.fmt.Sprintf("%w", err)does not wrap — it just produces%!w(error=...).- The wrapped error must be a direct argument, not a sub-expression that returns an error indirectly. Type-asserted things still work as long as the value satisfies
error. fmt.Errorf("%w", err)witherrbeing a typed nil wraps a non-nil interface holding a nil pointer. Tricky but logically consistent.- The order of arguments matters when you have multi-wrap: the multi-wrap struct stores them in argument order, and
errors.Iswalks them all.
Test¶
package errfmt
import (
"errors"
"fmt"
"testing"
)
var ErrSentinel = errors.New("sentinel")
func wrapIt(err error) error {
return fmt.Errorf("wrap: %w", err)
}
func TestWrap_PreservesIdentity(t *testing.T) {
err := wrapIt(ErrSentinel)
if !errors.Is(err, ErrSentinel) {
t.Fatalf("expected errors.Is true, got false")
}
}
func TestEmbed_LosesIdentity(t *testing.T) {
err := fmt.Errorf("wrap: %v", ErrSentinel)
if errors.Is(err, ErrSentinel) {
t.Fatalf("expected errors.Is false, got true")
}
}
Run with go test ./....
Tricky Questions¶
-
What is the signature of
fmt.Errorf?func Errorf(format string, a ...any) error. -
What does
%wdo that%vdoes not?%wwraps the argument, allowingerrors.Isanderrors.Asto find it.%vonly inserts the formatted text. -
What happens if you pass a non-error to
%w? The output contains%!w(<type>=<value>)and no wrapping occurs. No panic, no compile error. -
Can you have multiple
%win one call? Only since Go 1.20. Before that, only the first works; later ones become%!w(...). -
What is the result of
fmt.Errorf("%w", nil)? A non-nil error whose text is"%!w(<nil>)". Avoid wrapping nil. -
Why use
fmt.Errorfat all iferrors.Newexists? For runtime values in the message and for the wrapping semantics of%w.
Cheat Sheet¶
// Plain formatted error
fmt.Errorf("port %d invalid", 22)
// Wrap an error (preserve identity)
fmt.Errorf("op: %w", err)
// Embed text only (lose identity)
fmt.Errorf("op: %v", err)
// Multi-wrap (Go 1.20+)
fmt.Errorf("op: %w and %w", a, b)
// Verbs you will use: %s %d %q %v %w
// %w must be passed an error.
// One %w pre-1.20; many %w 1.20+.
// Detect what was wrapped
errors.Is(err, ErrSentinel)
errors.As(err, &typedTarget)
// What NOT to do
fmt.Errorf("oops: %w", "string") // not an error
fmt.Errorf("oops: %w", nil) // wraps nothing
fmt.Errorf("oops: %v", err) // hides identity
Self-Assessment Checklist¶
- I know that
fmt.Errorfreturns anerror, not a string. - I can list at least four format verbs.
- I can explain the difference between
%wand%vin one sentence. - I know
%wonly works insidefmt.Errorf. - I know multi-wrap requires Go 1.20+.
- I can read
errors.Is(err, ErrFoo)and predict whether it is true based on whether the wrap chain used%wor%v. - I avoid wrapping nil errors.
- I prefer
errors.Newfor static messages.
Summary¶
fmt.Errorf is Sprintf for errors, plus one extra trick. It formats runtime values into an error message, and with the %w verb it wraps an existing error so the chain stays inspectable. Use %w to preserve identity, use %v only when you genuinely want a flat string. Wrap with operation context: fmt.Errorf("op: %w", err) is the heart of idiomatic Go error propagation.
What You Can Build¶
- A small CLI that opens a file, reads it, parses JSON, validates fields — and at every failure, the error message reads as a chain of operations.
- A "stack-of-context" helper that wraps each layer of a request handler with the layer name.
- A retry helper whose final error is
fmt.Errorf("after %d attempts: %w", n, lastErr).
Further Reading¶
- Package fmt — Errorf
- Working with errors in Go 1.13
- Go 1.20 release notes — multiple
%w - Source:
$GOROOT/src/fmt/errors.go
Related Topics¶
- 01-error-handling-basics — the
errorinterface and theif err != nilidiom. - 03-errors-new — when
errors.Newis the right tool. - 05-wrapping-unwrapping-errors — the broader wrapping protocol and
errors.Is/errors.As. - 06-sentinel-errors —
var Err... = errors.New(...)exposed as API.
Diagrams & Visual Aids¶
fmt.Errorf("op: %w", inner)
outer (wrapError)
+---------+--------+
| msg | "op: <inner.Error()>" |
| err | -------> inner |
+---------+--------+
|
v
inner (errorString)
+-----+----------+
| msg | "x went wrong" |
+-----+----------+
errors.Is(outer, inner) walks outer.err → inner → nil → done.