I stared at the terminal for twenty minutes. The error message was ten lines long, referenced some file in the Nix store with a hash I'd never remember, and told me something about "infinite recursion" or "attribute missing" or... something. I don't even remember anymore. I just remember the feeling: Nix was speaking a language I didn't understand, and I was failing.
That was my first week with Nix. It took months before I could read an error message without my brain basically shutting down. But once you learn the pattern, once you understand what Nix is actually trying to tell you, the errors go from cryptic nonsense to useful hints.
Here's the thing nobody told me: Nix errors have a grammar. They follow a structure. Once you know how to parse them, they stop feeling like punishment and start feeling like maps. Pretty weird, but true.
The Anatomy of a Nix Error
Most Nix errors have four distinct layers. Let's look at a real one I hit last week:
error: infinite recursion encountered
at /nix/var/nix/profiles/per-user/root/channels/nixpkgs/lib/attrsets.nix:123:5
122| recursiveUpdate = x: y:
123| mapAttrs (name: val:
| ^
124| if builtins.isAttrs val && builtins.isAttrs x.${name} or null
… while evaluating a nested attribute set
… while calling the 'map' builtin
… in the expression at /home/user/flake.nix:42:8
Looks terrifying, right? But it's actually telling you four specific things. Here's the breakdown:
Error type (the first line): infinite recursion encountered. This is what went wrong, not necessarily where, but the class of problem.
Location (the second line): The file and line number where Nix crashed. Usually deep in nixpkgs. This is rarely where you need to fix anything.
Trace (the "… while" lines): The evaluation stack, showing the chain of function calls. Reading these bottom to top shows the path from your code to the crash site.
Your code (the last "… in" line): The last trace entry is almost always the closest to where you introduced the problem. This is where you start debugging.
The rule I learned: The error location at the top is where Nix crashed. Your bug is almost always in the last line of the trace, or just above it. Read the trace bottom-up. That's it. That's the whole trick.
The Five Error Classes You'll Actually See
Nix errors basically group into a handful of recurring types. Recognizing them on sight saves you most of the diagnostic work.
1. Infinite Recursion Encountered
This means Nix tried to evaluate an attribute that depends on itself. Common causes: using self or super incorrectly in an overlay, or a callPackage where an attribute refers back to the derivation being defined.
# Classic trap: referring to 'pkgs' before it's fully constructed
let
pkgs = import <nixpkgs> { overlays = [ overlay ]; };
overlay = final: prev: {
myPkg = pkgs.stdenv.mkDerivation { ... }; # ← should be 'final.stdenv'
};
in pkgs
Debug strategy: Add --show-trace to your nix build or nix eval call. The trace will point you to exactly which attribute triggered the cycle. Yeah, it's verbose, but it's worth it.
2. Value Is Not a Function / Not an Attribute Set
Nix's type system is lazy. Errors only surface during evaluation. This means you passed something of the wrong shape. Usually a missing argument, a typo in an attribute name, or forgetting to apply a function.
# Often caused by:
let
f = { foo }: foo + 1;
in
f 42 # passing an int to a function expecting an attrset
When the error says "value is not an attribute set" and points into nixpkgs, it usually means you've passed a derivation where a set of options was expected, or vice versa. For instance, calling pkgs.python3.withPackages and forgetting the lambda. I did this one twice last month.
3. Attribute Not Found
The most common error for nixpkgs newcomers. You're trying to access an attribute that doesn't exist on a set. Check spelling, check whether the package is in the right channel, and make sure you haven't confused a package set (like pkgs.python3Packages) with a package (pkgs.python3). Seriously, this trips up everyone at first.
error: attribute 'pythonPackages' missing
# The attribute set 'pkgs' does have this path, just named differently:
# pkgs.python3Packages.requests ← correct
# pkgs.pythonPackages.requests ← does not exist
4. Builder Failed with Exit Code N
This is a build-time error, not an evaluation error. Nix successfully evaluated your derivation, but the build script failed. The error message itself is almost never useful. The real information is in the build log.
error: builder for '/nix/store/...-my-package-1.0.drv' failed with exit code 1
For full logs, run:
nix log /nix/store/...-my-package-1.0.drv
Critical: Always run nix log immediately. The exit code alone tells you basically nothing. The log tells you everything: missing headers, failed tests, network errors, wrong phase outputs. Seriously, always check the log first.
5. Hash Mismatch
A fetchurl, fetchgit, or similar fetcher got content that doesn't match the declared hash. This is either a stale hash in your derivation, or the upstream source changed in place (which happens more than you'd hope).
error: hash mismatch in fixed-output derivation '/nix/store/...':
specified: sha256-AAAA...
got: sha256-BBBB...
Fix it by updating the hash. The "got" value is the correct one. Copy it into your derivation. For fetchgit, also update the rev if the source moved. Yeah, it's annoying when upstream changes things, but that's the price of reproducibility.
Reading the Trace Strategically
The "… while evaluating" lines form a stack trace in reverse evaluation order. Your instinct is to look at the top. Resist it. The top is the crash site, deep in nixpkgs or a library. The bottom is where your code called into the machinery.
"The trace is a telescope pointed backwards. The furthest point is where things broke. Your eye should start at the lens."
The mental model: Each "… while" is a stack frame. Nix was evaluating the bottom frame first: your flake.nix, your module, your overlay. It called into something, which called into something else, until finally it reached the frame that caused the failure.
My process:
Find the last "… in the expression" line. This is typically pointing at your code. Open that file and go to that line.
Read one frame up. The next "… while" line tells you what Nix was doing with your value: calling a function, iterating a list, constructing an attribute set.
Match the operation to the error type. If it says "while evaluating a nested attribute set" and the error is "infinite recursion", look for circular attribute references at your entry point.
Ignore frames that are entirely inside nixpkgs unless you're writing an overlay or patch. Those frames are symptoms, not causes.
When unsure, add
builtins.traceprobes. Dropbuiltins.trace "reached here: ${builtins.typeOf x}" xnear your suspect expression to confirm what's being evaluated and in what shape.
Practical Debugging Commands
Knowing the theory is one thing. These are the commands you'll actually reach for when things break:
# Always add --show-trace for eval errors
nix build .#myPackage --show-trace
# Evaluate a single attribute without building
nix eval .#packages.x86_64-linux.myPackage.name
# Enter a nix repl and poke at the value directly
nix repl
:lf . # load current flake
packages.x86_64-linux.myPackage.buildInputs
# Read the full build log after a builder failure
nix log /nix/store/HASH-myPackage-1.0.drv
# Inspect what a derivation will build without building it
nix derivation show .#myPackage
Leveling Up: A Better REPL
The default nix repl is barebones. After months of typing :lf . and manually navigating attribute sets, I built a custom repl.nix that preloads all my hosts, configs, and helper functions. It looks like this:
# filename: repl.nix
{
# Pass your host name when starting the repl
host ? "xenomorph",
...
}:
let
flake = builtins.getFlake (toString ./.);
inherit (flake.inputs.nixpkgs) lib;
in
{
inherit lib;
inherit (flake) inputs;
# Quick access to current host
c = flake.nixosConfigurations.${host}.config;
config = c;
co = c.custom;
pkgs = flake.nixosConfigurations.${host}.pkgs;
# Helper functions for debugging
keys = lib.attrNames; # List all attributes in a set
deps = pkg: map (p: p.name or "unknown") (pkg.buildInputs ++ pkg.nativeBuildInputs or []);
# Find where an option is defined (filters out nixos internals)
where = path: let
opt = lib.attrByPath (lib.splitString "." path) null
flake.nixosConfigurations.${host}.options;
decls = if opt != null && opt ? files then opt.files else [];
isMine = f: !(lib.hasInfix "nixos/modules/" (toString f));
in lib.filter isMine decls;
# Reload without exiting repl
reload = import ./repl.nix { inherit host; };
}
// flake.nixosConfigurations # Merge in all host configs
Why this is better:
-
keys pkgs: Instead of typing:p pkgsand scrolling through 50,000 attributes, get a clean list -
where "services.nginx.virtualHosts": Find exactly which file defines that option, filtered to ignore nixpkgs internals -
deps pkgs.hello: See build inputs for any package without digging through nixpkgs source -
c/config/co: Short aliases for the current host's config and custom options -
reload: Re-import the repl.nix without exiting (great when you're editing the repl itself)
Using it with a helper script:
#!/usr/bin/env bash
if [[ -f repl.nix ]]; then
nix repl --arg host '"xenomorph"' --file ./repl.nix "$@"
else
# Fallback to regular repl if not in a project with repl.nix
nix repl .
fi
Now when I hit an error, I can jump into the repl and inspect values immediately:
nix-repl> c.services.nginx.enable
true
nix-repl> where "services.nginx.virtualHosts"
[ /home/k1ng/nix/dotfiles.nix/modules/web/nginx.nix ]
nix-repl> keys co.persist.home
directories files
nix-repl> deps pkgs.neovim
[ "libuv" "msgpack" "tree-sitter" ... ]
The inspiration for this came from Brian McGee's post about Nix's slow feedback loop. The REPL becomes a real debugging tool, not just a calculator.
Pro tip: The nix repl is criminally underused. You can load any flake, inspect attribute values, call functions, and test expressions interactively, all without triggering a full build. I use it basically every day now.
Quick Reference
| Error | What It Means | First Thing to Try |
|---|---|---|
infinite recursion |
Circular attribute dependency | Check overlays and self/super usage |
not a function |
Wrong type passed | Check arg shapes with nix repl
|
attribute missing |
Typo or wrong channel | Run nix search nixpkgs <name>
|
builder failed |
Build-time error | Always read the full log with nix log
|
hash mismatch |
Update the hash | Copy the "got" value into your derivation |
| Reading traces | Read bottom-up | Your entry point is at the end, not the top |
Nix errors reward patience and a systematic reading strategy. The wall-of-red sensation disappears as soon as you know where to look first. That's almost always the bottom of the trace, not the top.
Once that clicks, debugging Nix feels less like archaeology and more like following a well-marked trail. Still annoying sometimes, honestly, but at least you know where you're going.
Resources
If you're stuck on a Nix error, these resources actually help:
- nix.dev Troubleshooting Guide - Official guide for common errors like database issues, broken binary caches, and macOS problems
- nix.dev Nix Language Basics - Understanding the language fundamentals makes error messages way clearer
-
Nix Manual: Built-in Functions - Reference for debugging tools like
builtins.trace - Nix Manual: nix repl - Documentation for interactive debugging
-
Nixpkgs Manual: Library Functions - When you need to understand what
libfunctions are doing -
NixOS Discord - The unofficial Discord is more helpful than you'd expect. Bring your
--show-traceoutput. - NixOS Discourse - Search here first. Most errors have been asked about before.
- NixOS Wiki - Actually has useful debugging tips mixed in with the outdated stuff
Top comments (0)