Why this matters
Nix errors are long, lazy, and point at where a value was forced, not where it was defined. Once you know the shape of a trace and the five messages that account for most failures, a wall of text becomes a two-line read. This lesson uses the real errors you met in earlier lessons.
Concept
The shape of a trace
Every error has frames introduced by … and one final error: line. Frames go from outer context down to the failure. Read it bottom-up: the error: line says what went wrong, the nearest frame above says where.
error:
… while evaluating a path segment
at /tmp/config.nix:8:31:
7| prod = prod;
8| url = "https://${prod.host}:${prod.port}";
| ^
9| }
error: cannot coerce an integer to a string: 3000Bottom line: an integer was used as a string. Frame above: in the interpolation at line 8, column 31. The caret marks the exact ${prod.port}. Fix: toString prod.port.
"Path segment" is Nix's name for one ${} chunk of a string. It does not mean a filesystem path.
--show-trace
By default Nix trims frames:
(stack trace truncated; use '--show-trace' to show the full, detailed trace)--show-trace adds the call-site frames: which function was called from where. Compare the same error from deep.nix, which calls render settings where render reads s.server.prt:
Without:
… while evaluating the attribute 'server.prt'
error: attribute 'prt' missing
Did you mean port?With --show-trace, two more frames appear above those:
… from call site
at /tmp/deep.nix:5:1:
5| render settings
… while calling 'render'
at /tmp/deep.nix:3:12:
3| render = s: "port=${toString s.server.prt}";Now you see the caller. In a flake, the extra frames are what tell you which module or overlay introduced a bad value. Always add --show-trace on the second attempt.
The five messages
attribute 'x' missing. You accessed a key that is not there. Nix suggests near matches (Did you mean port?). Causes: typo, // dropped a nested set (see Attrsets), or the value is a different shape than you assumed. Check with builtins.attrNames.
undefined variable 'x'. A bare name with no binding. Causes: used a inside a non-rec set, forgot let ... in, or relied on with pkgs; that is out of scope. Also what you get when a with is shadowed and you misspelled the shadowing name.
infinite recursion encountered. A thunk needs itself. Causes: rec cycles, an overlay using self where super was meant, a module option that reads its own value. Different from stack overflow; max-call-depth exceeded, which is a function calling itself without a base case. See Laziness and recursion.
cannot coerce an integer to a string. Interpolation or + with a non-string. toString it. The variant cannot add a string to an integer means the integer was on the left: 1 + "1".
function 'f' called with unexpected argument 'c' and its twin called without required argument 'a'. Attrset pattern mismatch. Add ... to accept extras, or fix the call. The error names both the function and the argument. See Functions.
Also frequent: expected a Boolean but found an integer from if 1 then, and syntax error, unexpected ',' from a comma in a list.
Printing from inside: builtins.trace
trace msg value prints msg to stderr and returns value. Wrap any expression without changing the result:
let x = 5; in builtins.trace "x is ${toString x}" (x * 2)trace: x is 5
10Non-string messages print as Nix values: builtins.trace { a = 1; } 42 prints trace: { a = 1; }. For a quick look at a value in a pipeline, lib.debug.traceVal prints and returns the same thing:
nix eval nixpkgs#lib.debug.traceVal --apply 'f: f (1 + 1)'trace: 2
2lib.debug.traceValFn (v: "got ${toString v}") formats first. lib.traceSeq forces the value deeply before printing, so you see contents rather than «thunk».
Because of laziness, a trace only fires if the wrapped value is forced. No output means that code path was never evaluated. That itself is information.
Bisection
When the trace is not enough:
- Reproduce in the REPL or
nix eval -f file.nix attrwith the smallest attribute that fails. - Replace half the expression with a literal (
"x",{ },[ ]). Does it still fail? Then the bug is in the other half. - Repeat. Three or four rounds narrows a 200-line file to one line.
- For a flake:
nix eval .#devShells.x86_64-linux.default.namefails faster thannix develop, andnix flake checkevaluates all outputs.
Parse errors come first and are cheapest: nix-instantiate --parse file.nix checks syntax without evaluating anything. A comma in a list or a missing semicolon shows up here with a line and column:
error: syntax error, unexpected '=', expecting ';'
at /tmp/bad.nix:1:11:
1| { a = 1 b = 2; }
| ^Try it
nix eval --expr '{ a = 1; }.b'error: attribute 'b' missing
at «string»:1:1:
1| { a = 1; }.b
| ^
Did you mean a?nix eval --expr 'builtins.trace "evaluated" 1 + (let x = builtins.trace "never" 2; in 0)'trace: evaluated
1Only one trace fired. x was bound but never used.
Exercise
This fails. Read the trace, name which of the five messages it is, state the root cause in one sentence, and fix it.
let
mkUrl = { host, port }: "https://${host}:${port}";
cfg = { host = "localhost"; port = 3000; tls = true; };
in
mkUrl cfgSolution
error: function 'mkUrl' called with unexpected argument 'tls'Pattern mismatch: cfg has tls, the pattern does not accept extras. Add .... Then a second error appears: cannot coerce an integer to a string: 3000, because port is an integer in interpolation. Both fixes:
let
mkUrl = { host, port, ... }: "https://${host}:${toString port}";
cfg = { host = "localhost"; port = 3000; tls = true; };
in
mkUrl cfg"https://localhost:3000"Laziness hid the second error behind the first. Fix, rerun, read again.
Trap
Fixing the line the caret points at when the real bug is upstream. attribute 'prt' missing at s.server.prt is a typo at that line. But attribute 'port' missing at the same place means the caller passed a set without port, and the fix is in the caller. The caret shows where Nix noticed, which for lazy code is often far from where the wrong value was built. --show-trace and builtins.trace on the input close that gap.