How do you keep a Go error message on one line when it embeds a filename supplied by a user?
answer
- the value came from outside your program
- a filename may contain a newline
- quote it rather than pasting it raw
- escapes control bytes, shows empty strings
- your words plain, their values quoted
basics
~20 sFormat the value with the %q verb rather than %s. It renders as a quoted Go string literal, escaping newlines, tabs and invalid UTF-8, so the message stays one line and an empty value stays visible.
solid answer
~50 sThe convention that an error message is one line without a trailing newline is easy to hold for text you wrote and easy to break with text you did not. A path, a header value, or a field name that came from input can contain a newline, a tab, a terminal escape sequence, or bytes that are not valid UTF-8, and `fmt.Errorf("read %s: %w", name, err)` will paste all of it in raw — splitting the message across lines and breaking anything that assumes one record per line. Using `%q` instead renders the value as a double-quoted Go string literal: control characters become escapes, invalid bytes become hex escapes, and the surrounding quotes make an empty string or one that is entirely spaces visible rather than an odd gap. It also makes the boundary of the value unambiguous when it sits next to your own words. The same reasoning says: truncate very large values before formatting them.
code
go · 5 linesdata, err := os.ReadFile(name)
if err != nil {
return nil, fmt.Errorf("read %q: %w", name, err)
}
return data, nilgo deeper
Know that interpolated values can contain line breaks and that quoting them with %q keeps the error on one line. Being able to say why an empty value should still be visible is a good sign.
Explain what %q actually produces — a quoted Go string literal with escapes for control bytes and invalid UTF-8 — and contrast it with pasting the value raw. Mention truncation on rune boundaries for large values.
Show the operational consequence: a message split by its own data becomes two log records and stops matching the searches your on-call runbook depends on. Offer the review rule of plain words, quoted inputs.
Argue for the convention across services so log parsing and alerting can rely on one record per error, and weigh the size of embedded values against log cost and readability.
## The invariant being protected A Go error message is one line of text with no trailing newline, because whoever finally prints it decides the formatting. That invariant is entirely under your control for the words you write — and entirely out of your control for the values you interpolate. ## Where the line break comes from Consider a tool that reports which file it could not read: ```go return fmt.Errorf("read %s: %w", name, err) ``` If `name` came from a command-line argument, a config file, an archive entry, or a request, it can legitimately contain almost any byte. On Unix a filename may contain a newline. A header value may contain a tab. A field copied out of a document may contain a carriage return or an ANSI escape sequence. Any of those pasted raw into the message means the rendered error is no longer one line: log ingestion sees two records, a `grep` for your prefix returns a fragment, and a terminal may even act on the escape sequence. ## The fix: %q ```go return fmt.Errorf("read %q: %w", name, err) ``` `%q` formats a string as a double-quoted Go string literal. That gives three properties that matter for error text: 1. **Escaping.** A newline is rendered as the two characters backslash and `n`, a tab likewise, and other control bytes as escapes. The message cannot be split by its own data. 2. **Delimitation.** The quotes mark where the value starts and ends. In `read "": ...` you can see that the name was empty; in `read : ...` you would be left wondering whether the formatting broke. A value that is entirely spaces is visible for the same reason. 3. **Byte safety.** Bytes that are not valid UTF-8 are rendered as hexadecimal escapes rather than being emitted raw into a terminal or a log. This is why so much standard-library error text quotes the offending value rather than pasting it: the message stays well-formed no matter what the input was. ## Runes, not bytes Go strings are immutable bytes that are usually — not always — UTF-8. Two consequences show up here. First, a message is measured by a reader in characters but stored in bytes, so a name of ten characters may be thirty bytes; truncating by slicing bytes can cut a multi-byte rune in half and produce a replacement character in the middle of your message. If you must truncate, truncate by runes, or truncate and then quote. Second, printable non-ASCII text — a filename in Cyrillic or Japanese — is legitimate and `%q` leaves it readable rather than mangling it; the escaping applies to control and invalid bytes. ## Size, not just shape The same instinct applies to length. Interpolating an entire request body, an entire query result, or a multi-megabyte string into an error message produces something no human will read and that will dominate a log bill. Bound what you embed: a prefix of the value plus an indication that it was truncated is more useful than the whole thing, and it keeps the message scannable. ## What quoting does not do Quoting is a *rendering* concern, not a safety boundary. It makes the message well-formed and unambiguous; it does not decide whether that value belonged in a user-visible message in the first place, and it is not a sanitiser for any downstream system. Nor does it excuse a trailing newline: never append `\n` yourself, quoted or not. ## The habit A simple review rule covers nearly all of it: **your own words go in as plain text; anything that came from outside your program goes in quoted.** Applied consistently it makes every message in a codebase parse the same way, keeps the one-line invariant true even for hostile input, and removes a whole class of confusing bug reports where the pasted error was missing half of itself.
- What does %q show in an error message that %s hides?The boundaries of the value, so an empty or whitespace-only string is visible rather than an unexplained gap; escaped control characters instead of literal line breaks and tabs; and hex escapes for bytes that are not valid UTF-8, which %s would emit raw into a terminal or log.
- The value you want to embed is a megabyte long. What do you do?Bound it before formatting. Take a short prefix and mark that it was cut, so the message stays scannable and does not dominate a log line. If you truncate a string yourself, cut on rune boundaries so you do not leave half of a multi-byte character in the message.
- Is quoting the value enough to make a message safe to show a user?No. Quoting only guarantees the message is well-formed and stays on one line. Whether that particular value should appear in text a user sees is a separate judgement, made before you format it, not solved by the verb you chose.
saying these in an interview costs you the question
- Assumes a filename cannot contain a newline
- Strips control characters by hand instead of quoting
- Appends a newline to the message to force a line break
- Interpolates an entire request body into the message
- Truncates a UTF-8 value by slicing bytes and splits a rune
- Thinks quoting the value makes it safe to display