AlgoMaster Logo

Creating Errors (errors.New, fmt.Errorf)

Last Updated: May 22, 2026

High Priority
10 min read

Go gives you two everyday tools for building error values: errors.New from the errors package and fmt.Errorf from the fmt package. The first one wraps a fixed message in an error. The second one formats values into the message the way fmt.Sprintf does. This lesson covers when to use each, the conventions every Go error message follows, and how the values they produce behave when you compare or reuse them.

errors.New: Wrapping a Fixed Message

errors.New is the simplest way to produce a value that satisfies the error interface. You give it a string, and it gives you back an error whose Error() method returns that string.

The function lives in the standard library's errors package, so you have to import it. The return type is the error interface. Under the hood errors.New allocates a tiny struct that holds the string and exposes an Error() method, but the only thing your code sees is the error value.

Use errors.New when the message is static. If the message never needs to change based on a variable, formatted value, or runtime state, errors.New is the right choice. A common shape is a function that returns an error for an invalid input:

The function returns the error built by errors.New when the cart has zero items. The caller checks err != nil, prints the message, and moves on. The message itself is hardcoded, so there's no need for formatting.

fmt.Errorf: Building a Message From Values

Static messages aren't always enough. When the message needs to include a quantity, a product code, an ID, or any other runtime value, use fmt.Errorf instead. It works like fmt.Sprintf: same verbs, same rules, same arguments. The difference is that fmt.Errorf returns an error rather than a string.

The %s verb gets replaced with the value of code, the resulting string becomes the error's message, and fmt.Errorf hands back an error value. You don't need to import errors for this; fmt.Errorf is enough on its own.

%v is the verb for cases where the value's type isn't fixed. It uses the default formatting for whatever type you pass:

Three different types, three %v verbs, one error message. The verbs are the same ones fmt.Sprintf uses, so %d for integers, %f for floats, %q for quoted strings, and %v for "default formatting" all work the same way they do in any other fmt call.

One verb you might have seen elsewhere is %w. That's the "wrap" verb, and it does something different from the formatting verbs. For this lesson, stick to the formatting verbs (%s, %d, %v, %q, and friends).

The error message includes the bad input value, which is much more useful for debugging than a generic "invalid amount". When a customer reports a problem, the logs show exactly which number caused the failure.

errors.New vs fmt.Errorf: Picking One

The two functions overlap. Anything you can do with errors.New you can also do with fmt.Errorf (just don't include any format verbs). So which one should you use?

If the message is a fixed string, use errors.New. If the message includes a value computed at runtime, use fmt.Errorf.

SituationUse
Fixed message, no values to interpolateerrors.New("cart is empty")
Message includes a variable, count, or IDfmt.Errorf("cart has %d items, max is 50", n)
Message needs a quoted string or other format verbfmt.Errorf("invalid email %q", email)
Wrapping another errorfmt.Errorf("place order: %w", err)

The diagram shows the decision in one picture. Start with the question "does the message include runtime values?" If no, use errors.New. If yes, use fmt.Errorf. A third path, wrapping with %w, sits off to the side, but it's worth knowing it exists so you don't mistake fmt.Errorf for being "just formatting".

Here's the same validation function written both ways so you can see the difference side by side:

Both errors flag the same problem. The formatted version is more helpful in logs because it tells you which invalid value showed up. If you only have the static message, you have to dig into the input separately to figure out what went wrong. For anything customer-facing, lean toward the formatted version so the message carries context.

One small caveat. fmt.Errorf does a bit more work than errors.New because it has to walk the format string and run the verbs. For a single call this is invisible. In a tight loop that builds millions of errors per second, the difference is measurable but still small. Don't pick errors.New for performance; pick it for clarity when there's nothing to format.

Conventions for Error Message Strings

Go has informal conventions for how error messages should read. They aren't enforced by the compiler, but the standard library follows them and golint will flag code that doesn't. Errors get logged, concatenated, and wrapped by other code, and a consistent style keeps the combined output readable.

The four rules are:

  • Start with a lowercase letter.
  • No trailing punctuation. No period, no exclamation mark.
  • Keep it short. One clause is enough.
  • Describe what failed, not what should have happened.

Look at the last two lines. When another piece of code prefixes "place order:" in front of the message, the lowercase version reads naturally and the capitalized version reads like two separate sentences awkwardly stitched together. The trailing period in the second one makes it worse because now the combined message has a period in the middle followed by no period at the end. Lowercase, no trailing period, and the message composes cleanly with whatever wraps it.

The Go FAQ and the official "Error handling and Go" blog post both spell this out, and the standard library is consistent about it. os.ErrNotExist's message is "file does not exist". io.EOF's message is "EOF" (an acronym, so all caps, but no trailing period). strconv.ErrSyntax's message is "invalid syntax". None of them start with a capital letter unless the first word is a proper noun or acronym, and none of them end with punctuation.

The inner message ("email is required") is lowercase and unpunctuated. The outer function adds its own context ("register customer: ") and concatenates, and the result reads as one clean line. If the inner message had been "Email is required." you'd get "register customer: Email is required." which has a stray capital letter mid-message and a confusing period before the end.

A second convention worth following: describe what went wrong, not what the caller should do. "cart is empty" beats "please add items to your cart". The error is for the program, not the customer. User-facing wording is the responsibility of whatever layer turns the error into a UI message. Keep the error itself factual.

Formatting Multiple Values Into One Error

Validation errors often need to mention several things at once. fmt.Errorf handles this the same way fmt.Sprintf does: pass as many arguments as you have verbs, and the function fills them in.

Three values are stitched into one message. The order ID identifies which order failed, the count tells you the offending value, and the maximum tells you the rule. Together they give you everything you need to investigate without digging through other logs. If the message were just "too many items", you'd have to find the order separately to know which one.

For validation rules that can fail for several reasons at once, you have two choices. Return the first failure (cheaper, simpler), or collect them all and return a single error that lists every problem. The collected-failures version uses strings.Join or its own format string:

The function builds a list of problems, joins them with "; ", and wraps the joined string in a single error with errors.New. Notice that we use errors.New for the outer error because the final string is already built. We use fmt.Sprintf (not fmt.Errorf) to build the age fragment because we want a string to append to the slice, not an error value. The choice between Sprintf and Errorf follows the same rule as the choice between Println and Sprintln: pick the one whose return type matches what you need.

Errors As Values: Identity vs Equality

Every call to errors.New returns a new pointer to a new internal struct, even when you pass the same string twice. Two errors that print identically aren't necessarily the same error.

The two errors print the same message but compare unequal. That's because errors.New creates a fresh value each call, and Go's == on interface values compares the type and the underlying pointer. Two distinct pointers to two distinct heap allocations are different values, even if those allocations hold the same string.

Go's standard library uses this as a feature. If error identity were based on the message string, anyone could "spoof" a specific error by returning their own errors.New with the same text, and code that branches on the error value could be tricked. By making each call unique, the language guarantees that the only way to get a specific error value is to reference the exact one that was created somewhere.

The flip side of this rule is that you should create a "well-known" error once and reuse the same value everywhere. That way callers can compare against your single shared value, and the comparison actually means something:

The package declares ErrCartEmpty exactly once with errors.New. Every call site that wants to signal "the cart was empty" returns this same variable. Callers can then check err == ErrCartEmpty and know they got that specific failure, not just some other error whose message happens to match. This pattern has a name, the sentinel error pattern. The point here is just that errors.New returns a unique value each call, and that property is the reason the sentinel pattern works.

The diagram shows the same call repeated twice. Each call allocates its own internal struct and returns a pointer to it. The == comparison checks pointer identity, finds the two pointers point at different allocations, and returns false. To get equal values you have to call errors.New once and store the result somewhere you can reference from both sites.

The same uniqueness rule applies to fmt.Errorf. Each call returns a new error value, and comparing two fmt.Errorf results with == is almost always false (and almost always pointless, because the values include formatted state that's expected to differ).

Returning Errors From Functions

The shape of a function that can fail in Go is almost always the same: it returns the regular result first and an error second. Callers check the error before using the result. Inside the function, you build the error with errors.New or fmt.Errorf and return it as soon as the failure is detected.

Three failure paths, three different error messages, each one built with fmt.Errorf because each one needs to mention the specific values that caused the failure. The success path returns the product and nil. The caller checks err first, prints the message if the call failed, and only uses the product when err == nil.

Notice the order of checks inside reserveStock. The cheapest check (quantity sign) runs first, the lookup runs next, and the stock comparison runs last. This isn't a hard rule, but it's a common pattern: short-circuit on the easy stuff before doing the expensive work. For now, the takeaway is that errors.New and fmt.Errorf are the building blocks that produce the values you return.

One thing to watch for is the zero value you return alongside the error. The convention in Go is that when a function returns (T, error) and the error is non-nil, the T value is undefined and the caller should not use it. The function above returns Product{} (the zero value of Product) in every error path, which makes the contract obvious and avoids any accidental use of a half-built struct.

Why Message Style Matters Beyond a Single Call

The conventions for error messages (lowercase, no trailing punctuation, factual) feel pedantic until you see what happens when errors get concatenated. Real programs almost never return an error as-is. They add context, log it, ship it to an aggregator, and sometimes wrap it before passing it up the call stack. Each of those steps composes the original message with more text.

Three layers, three messages, one line of output. Each layer prepends a short prefix ("handle request: ", "place order ORD-501: ") and uses the lower layer's message verbatim. Because every individual message follows the convention (lowercase start, no trailing period), the final concatenated string reads as a clean breadcrumb trail from the outer call all the way down to the underlying failure. Anyone reading the log can scan from left to right and follow the path.

Now imagine the same chain with messages that violate the conventions:

That output is still readable, but a log aggregator that splits on capital letters or sentence-ending punctuation could trip over it. More importantly, when you grep through millions of log lines, you can't reliably distinguish "this is the start of an error" from "this is a middle clause". Following the Go conventions keeps every concatenation deterministic, and the longer your call chains get, the more that matters.

This is why the conventions exist as conventions rather than as personal preferences. They don't matter for one call in isolation. They matter the moment errors start composing, which is what they're going to do in any real Go program. For now, write each individual message as a clean lowercase fragment, and the wrapping in the next chapters will compose cleanly on top of it.