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:8

Looks 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 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.

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.

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 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 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:

  1. Find the last "… in the expression" line. This is typically pointing at your code. Open that file and go to that line.

  2. 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.

  3. 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.

  4. Ignore frames that are entirely inside nixpkgs unless you're writing an overlay or patch. Those frames are symptoms, not causes.

  5. When unsure, add builtins.trace probes. Drop builtins.trace "reached here: ${builtins.typeOf x}" x near 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 .#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 pkgs and 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.

Quick Reference

ErrorWhat It MeansFirst Thing to Try
infinite recursionCircular attribute dependencyCheck overlays and self/super usage
not a functionWrong type passedCheck arg shapes with nix repl
attribute missingTypo or wrong channelRun nix search nixpkgs <name>
builder failedBuild-time errorAlways read the full log with nix log
hash mismatchUpdate the hashCopy the "got" value into your derivation
Reading tracesRead bottom-upYour 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:

FAQ

Q Why does the error location point to nixpkgs instead of my code?
A Nix crashes where it detects the problem, which is usually deep in nixpkgs library code. Your mistake is almost always at the bottom of the trace—the last "… in" line. That is where your code called into the machinery. Ignore the nixpkgs location at the top unless you are writing an overlay or patching nixpkgs itself.
Q How do I use --show-trace without drowning in output?
A Start from the bottom and read upward. Your code typically has readable paths like /home/user/project/ or /nix/store/...-source/. Nixpkgs internals have paths with hashes followed by nixpkgs (like /nix/store/abc123-nixpkgs/lib/attrsets.nix). Look for the last frame with your project files—that is where the problem originated. Stop there and ignore everything above unless you are debugging nixpkgs itself.
Q What is the difference between evaluation errors and build errors?
A Evaluation errors happen while Nix is calculating what to build. These include missing attributes, type mismatches, and infinite recursion. Build errors happen during actual compilation: missing headers, failed tests, network timeouts. Evaluation errors need `--show-trace`. Build errors need `nix log` to see the full build output.
Q Why are Nix errors so much worse than other package managers?
A Nix is a lazy functional language with complex evaluation semantics. Errors surface far from where the mistake was made because evaluation is deferred. The upside is that once you learn the pattern, the errors become predictable. Read bottom-up and look for the five error types. They are verbose but consistent.
Q Can I make Nix show better error messages?
A Not really. The Nix team has been working on error message improvements, but the fundamental constraint is the lazy evaluation model. What you can do is use the REPL to inspect values interactively, add `builtins.trace` calls to track evaluation flow, and always use `--show-trace` for evaluation errors. The repl.nix setup in this article makes interactive debugging much easier.
Asaduzzaman Pavel

About the Author

Asaduzzaman Pavel is a Software Engineer who actually enjoys the friction of a well-architected system. He has over 15 years of experience building high-performance backends and infrastructure that can actually handle the real-world chaos of scale.

Currently looking for new opportunities to build something amazing.