When do you use errors.New versus fmt.Errorf to construct an error in Go?
answer
- one of them takes a format string
- is the text known before the program runs?
- %q and %d decide it
- two calls, two different values
- neither is compiler magic
basics
~20 sUse errors.New when the message is fixed text. Use fmt.Errorf when the message must interpolate runtime values, since it takes a format string and arguments. Both return a value satisfying the error interface, and neither is special to the compiler.
solid answer
~50 s`errors.New(text string) error` builds an error whose message is exactly the string you hand it - use it when nothing about the message varies. `fmt.Errorf(format string, a ...any) error` runs the `fmt` formatter first, so it is the right call whenever a runtime value belongs in the message: `fmt.Errorf("unrecognised answer %q", answer)`. Calling `fmt.Errorf` with no verbs is just a slower `errors.New`, and building a message with `+` concatenation instead of a verb is a readability loss and does not even compile for non-string operands. One property worth knowing: each `errors.New` call returns a fresh, distinct value, so two calls with identical text are not `==` to each other. And if callers need to *branch* on the failure rather than read it, neither function is the answer - that calls for a value or type the caller can recognise.
code
go · 9 linesfunc confirm(answer string) error {
if answer == "" {
return errors.New("no answer given")
}
if answer != "y" && answer != "n" {
return fmt.Errorf("unrecognised answer %q", answer)
}
return nil
}go deeper
Know both signatures and the rule of thumb: fixed text goes to errors.New, a message that needs a runtime value goes to fmt.Errorf with a format verb.
Explain that fmt.Errorf runs the formatter, that verbs like %q protect against invisible input, and that each errors.New call yields a distinct value rather than an interned one.
Show when a message is the wrong container entirely - callers needing to branch or read structured data should get a value or type to work with, not a sentence to parse.
Own the house rule. Decide when a failure is worth promoting from a message to a named part of a package's contract, since anything callers can match on becomes something you cannot reword later.
## Two functions, one interface Both constructors exist to save you from declaring a type every time a function needs to say "this went wrong": ```go func New(text string) error // package errors func Errorf(format string, a ...any) error // package fmt ``` Neither is a language feature. `errors.New` returns a pointer to a tiny unexported struct that stores the string and implements `Error() string`; `fmt.Errorf` formats its arguments and returns a similar value. You could write either yourself in five lines. They exist because most errors really are just a sentence. ## The dividing line: does the message vary? If every occurrence of the failure produces the same words, `errors.New` is enough: ```go return errors.New("no answer given") ``` If the message must carry a value known only at runtime - the field that failed, the value the user typed, a count, a duration - reach for `fmt.Errorf`: ```go return fmt.Errorf("unrecognised answer %q", answer) ``` The `%q` there is doing real work: it quotes and escapes the user's input, so an empty answer prints as `""` rather than vanishing, and a stray tab is visible. `%d`, `%v` and `%q` in an error message are worth choosing deliberately, exactly as in any other formatting call. What you should not do is smuggle values in by concatenation. `errors.New("port out of range: " + port)` does not compile when `port` is an `int`, and the `strconv`-plus-`+` version that does compile is longer and harder to read than one format string. Equally, `fmt.Errorf("no answer given")` with no verbs at all runs the formatter for nothing; `go vet`'s `printf` check will also complain if you pass arguments that no verb consumes, or a verb with no argument. ## Each call makes a distinct value A point that surprises people the first time: `errors.New` does not intern or deduplicate. ```go a := errors.New("timeout") b := errors.New("timeout") // a == b is false: each call produced a different value. ``` The returned type is a *pointer*, so equality is identity, not text. This is deliberate - it means two unrelated packages that happen to phrase a failure the same way never collide. The practical consequence is that if you want callers to be able to recognise a specific failure, the value has to be created once and shared, or the failure has to be carried by a type the caller can extract; constructing a fresh error at the call site and hoping someone matches its text is the fragile alternative. How callers then recognise the failure is a separate design step with its own machinery. ## Cost, and when it matters Both constructors allocate: a small struct, plus the boxing into the `error` interface. `fmt.Errorf` additionally runs the formatter, which is meaningfully more expensive than copying a string. For ordinary error paths this is irrelevant - you are about to do something far more expensive, like return up a call stack and log. It starts to matter when an "error" is being produced in a hot loop as a normal outcome rather than an exceptional one, which is usually a sign the design should not be using an error there at all. If a fixed error genuinely is produced thousands of times a second, construct it once and return the same value rather than rebuilding identical text on every call. ## When neither is right Both functions produce an error whose entire content is a human-readable sentence. That is the wrong shape when: - the caller must make a decision based on *which* failure occurred, or - the caller needs the structured data inside the message - the field name, the offending value, an HTTP status - and would otherwise have to parse the string back out. Parsing `Error()` output is the anti-pattern this leads to: the message becomes an accidental API, and the next person who improves the wording breaks a caller. When callers need data or identity, define a type that carries fields and implements `Error() string`, or share a single agreed value. `errors.New` and `fmt.Errorf` are for the common case where the message is genuinely the whole payload. ## A note on the verbs `fmt.Errorf` also understands `%w`, which does something the other verbs do not: it keeps the operand error reachable rather than just copying its text. That belongs to the discussion of wrapping; the thing to hold onto here is that reaching for `fmt.Errorf` is what puts that option on the table, whereas `errors.New` can only ever produce a flat string.
- Does it cost anything to call errors.New on every failure?A small allocation for the value plus the boxing into `error` - negligible on a real error path. It only matters when a fixed error is produced in a hot loop, and the fix there is to build the value once and return the same one rather than rebuilding identical text on each call.
- You have runtime values to report, but callers need to act on them. Is fmt.Errorf still right?No. Once a caller needs the field name, the offending value or a status code as *data*, putting them in a message forces string parsing and turns your wording into an API. Define a type with those fields and an `Error() string` method instead, so the text stays free to change.
- Could you build an error without either function?Easily - both are ordinary library code. Any value whose type declares `Error() string` is an error, so a three-line named type does the job. `errors.New` and `fmt.Errorf` are conveniences for the overwhelmingly common case where the message is the entire content of the failure.
saying these in an interview costs you the question
- Thinks fmt.Errorf exists only for wrapping
- Uses fmt.Errorf with no verbs where errors.New fits
- Believes two errors.New calls with equal text are ==
- Builds messages by concatenation instead of a verb
- Calls errors.New a compiler builtin