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 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.
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:8Looks terrifying, right? But it's actually telling you four specific things:
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.
The Five Error Classes You'll Actually See
Nix errors 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 pkgsDebug strategy: Add --show-trace to your nix build or nix eval call. The trace will point you to exactly which attribute triggered the cycle.
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 attrsetWhen 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.
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).
error: attribute 'pythonPackages' missing
# The attribute set 'pkgs' does have this path, just named differently:
# pkgs.python3Packages.requests ← correct
# pkgs.pythonPackages.requests ← does not exist4. 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.drvCritical: Always run nix log immediately. The exit code alone tells you nothing. The log tells you everything: missing headers, failed tests, network errors, wrong phase outputs.
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.
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:
# 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 .#myPackageLeveling 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 configsWhy this is better:
keys pkgs: Instead of typing:p pkgsand scrolling through 50,000 attributes, get a clean listwhere "services.nginx.virtualHosts": Find exactly which file defines that option, filtered to ignore nixpkgs internalsdeps pkgs.hello: See build inputs for any package without digging through nixpkgs sourcec/config/co: Short aliases for the current host's config and custom optionsreload: 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 .
fiNow 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.
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 is 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, 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
