The Nix REPL and Debugging Expressions
The Nix REPL and Debugging Expressions
The boring default before editing any Nix file is opening nix repl .# first. It gives you an interactive evaluator for the current flake, lets you inspect types and attribute paths without touching the file system, and surfaces errors instantly — the same role Python’s REPL or ghci plays for Haskell.
Mental model
nix repl is an incremental expression evaluator. Each line you type is parsed and evaluated; the result is printed. The session accumulates bindings: assign with x = 42 and x stays in scope for the rest of the session.
Key REPL meta-commands:
| Command | What it does |
|---|---|
:t expr |
Print the inferred type of expr |
:e expr |
Open the definition of expr in $EDITOR |
:b drv |
Realise a derivation (nix-store --realise) |
:p expr |
Pretty-print a value, expanding lazy thunks |
:l path |
Load a .nix file or flake into scope |
:r |
Reload — re-evaluate all :l expressions |
:? |
List every meta-command |
There are two common launch modes:
nix repl # empty scope
nix repl 'nixpkgs' # loads nixpkgs; pkgs = import <nixpkgs> {}
nix repl .# # loads the flake in the current directoryNix evaluation is lazy. The REPL only evaluates what you ask for. An attribute set with a broken nested path will not error until you actually access that path.
Worked examples
Case 1: Basic REPL session
This walkthrough covers arithmetic, strings, attribute sets, and function application — the primitives you will inspect constantly at the infrastructure desk.
Start the REPL with no arguments:
nix replOutput:
Nix 2.35.2
Type :? for help.
nix-repl>
Arithmetic:
nix-repl> 2 + 2
4
nix-repl> 1024 * 1024
1048576
nix-repl> 7 / 2
3
Division is integer division for two integers. Use floats explicitly if you need fractional results.
Strings and interpolation:
nix-repl> "desk-" + "api"
"desk-api"
nix-repl> host = "db.desk.corp"
nix-repl> "connecting to ${host}:5432"
"connecting to db.desk.corp:5432"
Attribute sets:
nix-repl> svc = { name = "billing"; port = 8080; }
nix-repl> svc.name
"billing"
nix-repl> svc.port
8080
Function application:
nix-repl> greet = name: "Hello, ${name}!"
nix-repl> greet "ops-team"
"Hello, ops-team!"
Type introspection with :t:
nix-repl> :t 42
an integer
nix-repl> :t "desk-api"
a string
nix-repl> :t svc
a set
nix-repl> :t greet
a function
nix-repl> :t [ 1 2 3 ]
a list
nix-repl> :t true
a Boolean
:t is the fastest way to catch a “I expected a string but got a set” mismatch before it becomes a build error.
Case 2: Loading nixpkgs
nix repl 'nixpkgs' imports nixpkgs and places its attribute set into scope. This is the standard way to inspect package metadata, library functions, and attribute paths at the infrastructure desk.
nix repl 'nixpkgs'Output:
Nix 2.35.2
Type :? for help.
Loading installable 'nixpkgs'...
Added 22756 variables.
nix-repl>
Query a package version:
nix-repl> go.version
"1.22.3"
nix-repl> postgresql.version
"16.3"
nix-repl> prometheus.version
"2.51.2"
Inspect package metadata:
nix-repl> go.meta.description
"The Go Programming Language"
nix-repl> go.meta.homepage
"https://go.dev/"
Browse lib functions. Every function under lib is an attribute — tab-complete works:
nix-repl> lib.strings.toUpper "hello"
"HELLO"
nix-repl> lib.strings.concatStringsSep ", " [ "api" "worker" "scheduler" ]
"api, worker, scheduler"
nix-repl> lib.lists.flatten [ [ 1 2 ] [ 3 [ 4 5 ] ] ]
[ 1 2 3 4 5 ]
nix-repl> lib.attrsets.mapAttrs (k: v: v + 1) { a = 1; b = 2; }
{ a = 2; b = 3; }
Jump to a function’s source with :e:
nix-repl> :e lib.strings.concatStringsSep
This opens lib/strings.nix at the definition of concatStringsSep in your $EDITOR. It is the fastest way to understand what a library function actually does without searching the nixpkgs repository manually.
Check whether an attribute exists before using it:
nix-repl> lib.attrsets.hasAttr "version" go
true
nix-repl> lib.attrsets.hasAttr "doesNotExist" go
false
Case 3: Loading a flake
nix repl .# loads the flake in the current directory. Every top-level output — packages, devShells, nixosConfigurations, apps — becomes directly accessible in scope.
The infrastructure desk flake used throughout this section:
# flake.nix
{
description = "Desk infrastructure flake";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
outputs = { self, nixpkgs }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
in
{
packages.${system}.default = pkgs.writeShellApplication {
name = "desk-hello";
text = ''
echo "Infrastructure desk tooling — online."
'';
};
devShells.${system}.default = pkgs.mkShell {
packages = [ pkgs.go pkgs.gopls pkgs.golangci-lint ];
shellHook = ''
echo "desk devShell ready"
'';
};
nixosConfigurations.workstation = nixpkgs.lib.nixosSystem {
inherit system;
modules = [ ./hosts/workstation/configuration.nix ];
};
};
}Load the flake:
nix repl .#Output:
Nix 2.35.2
Type :? for help.
Loading installable '.#'...
Added 7 variables.
nix-repl>
Explore outputs:
nix-repl> :t packages
a set
nix-repl> packages.x86_64-linux.default.name
"desk-hello"
nix-repl> :t devShells.x86_64-linux.default
a derivation
nix-repl> devShells.x86_64-linux.default.buildInputs
[ «derivation /nix/store/...-go-1.22.3.drv» ... ]
Explore NixOS configuration options:
nix-repl> nixosConfigurations.workstation.config.networking.hostName
"workstation"
nix-repl> :t nixosConfigurations.workstation.config.environment.systemPackages
a list
nix-repl> :p nixosConfigurations.workstation.options.networking.hostName.definitionsWithLocations
[ { file = "/home/desk/infra/hosts/workstation/default.nix"; value = "workstation"; } ]
.definitionsWithLocations returns every module file and value that contributed to an option. When an unexpected service or package lands in your closure, query .options.<path>.definitionsWithLocations to pinpoint which nixpkgs module or import enabled it.
Build a package directly from the REPL with :b:
nix-repl> :b packages.x86_64-linux.default
Output:
This derivation produced the following outputs:
out -> /nix/store/r7kzxq3j4b5c6d7e8f9g-desk-hello
After :b succeeds you can inspect the output path in the shell without leaving the session. :b is the fastest build-test loop available: no terminal switching, no retyping the flake attribute path.
After editing flake.nix, reload without restarting:
nix-repl> :r
Output:
Loading installable '.#'...
Added 7 variables.
Case 4: Debugging an expression with a wrong attribute path
The infrastructure desk’s shell.nix has a typo — pkgs.golint instead of the correct pkgs.golangci-lint. Reproduce the error and then isolate it in the REPL.
Save the broken file:
# shell.nix
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
packages = [
pkgs.go
pkgs.golint # wrong: this attribute was removed from nixpkgs years ago
pkgs.gopls
];
}Evaluate with nix-instantiate --eval:
nix-instantiate --eval shell.nixOutput:
error: attribute 'golint' missing
at /nix/store/...-source/pkgs/top-level/all-packages.nix:9423:5:
9422| golang-migrate = callPackage ../tools/misc/golang-migrate { };
9423| golint = throw "golint was removed from nixpkgs. Use golangci-lint instead.";
9424|
… while evaluating the attribute 'golint'
at /home/ops/desk/shell.nix:6:5:
5| pkgs.go
6| pkgs.golint
7| pkgs.gopls
The error is an evaluation-time error: Nix never started building anything. Now reproduce the same inspection in the REPL to narrow it down interactively:
nix repl 'nixpkgs'nix-repl> pkgs = import <nixpkgs> {}
nix-repl> :t pkgs.go
a derivation
nix-repl> :t pkgs.golangci-lint
a derivation
nix-repl> :t pkgs.golint
error: attribute 'golint' missing
… while evaluating the attribute 'golint'
The REPL pinpoints exactly which path is broken. Fix the file:
# shell.nix
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
packages = [
pkgs.go
pkgs.golangci-lint # correct attribute name
pkgs.gopls
];
}Re-evaluate:
nix-instantiate --eval shell.nixOutput:
«derivation /nix/store/...-nix-shell.drv»
Case 5: builtins.trace for printf-style debugging
When a conditional chooses the wrong branch or a version check silently produces unexpected output, builtins.trace prints a diagnostic line during evaluation and then returns its second argument unchanged. It is the Nix equivalent of a print statement.
Save as default.nix:
# default.nix
let
pkgs = import <nixpkgs> {};
# Confirm which version of a package is selected at eval time.
tracedPkg = pkg:
builtins.trace
"checking version: ${pkg.pname}-${pkg.version}"
pkg;
selectedGo = tracedPkg pkgs.go;
in
{
inherit selectedGo;
name = selectedGo.pname;
}Evaluate:
nix eval --file default.nix nameOutput:
trace: checking version: go-1.22.3
"go"
The trace: prefix appears on stderr; the final value ("go") appears on stdout. Scripts that parse nix eval output are unaffected by trace lines.
Chain multiple traces to track a pipeline of transformations:
# default.nix
let
pkgs = import <nixpkgs> {};
step1 = builtins.trace "step1: selecting go" pkgs.go;
step2 = builtins.trace "step2: selecting gopls" pkgs.gopls;
step3 = builtins.trace "step3: building list" [ step1 step2 ];
in
step3Evaluate:
nix eval --file default.nixOutput:
trace: step1: selecting go
trace: step2: selecting gopls
trace: step3: building list
[ «derivation /nix/store/...-go-1.22.3.drv» «derivation /nix/store/...-gopls-0.15.3.drv» ]
Because Nix is lazy, traces only fire when the binding is actually forced. If step2 were never referenced elsewhere, its trace would never appear. This makes builtins.trace a precise tool: it confirms a value was evaluated, not just defined.
Remove all builtins.trace calls before committing. They print to stderr on every evaluation, including CI and deployment pipelines, producing confusing noise in logs.
The trap
Confusing evaluation-time errors with build-time errors.
An evaluation-time error (error: attribute 'foo' missing, error: value is a set while a string was expected) is detected by the Nix evaluator before any builder process starts. It appears immediately in nix eval, nix repl, and nix build — with no build log.
A build-time error (a compiler error, a failing test, a missing source file) is detected by the builder process after the evaluator has successfully constructed the derivation. It appears in the build log.
Evaluation-time error — REPL:
nix-repl> :b packages.x86_64-linux.typo
error: flake output attribute 'packages.x86_64-linux.typo' is not a derivation or path
No .drv was written. The evaluator rejected the expression before attempting to build.
Evaluation-time error — CLI:
nix build .#packages.x86_64-linux.typoerror: flake output attribute 'packages.x86_64-linux.typo' is not a derivation or path
Build-time error — REPL:
nix-repl> :b packages.x86_64-linux.desk-service
building '/nix/store/abc123-desk-service.drv'...
error: builder for '/nix/store/abc123-desk-service.drv' failed with exit code 1;
last 10 log lines:
> ./cmd/main.go:12:2: undefined: config.LoadEnv
Build-time error — CLI:
nix build .#packages.x86_64-linux.desk-serviceerror: builder for '/nix/store/abc123-desk-service.drv' failed with exit code 1;
last 10 log lines:
> ./cmd/main.go:12:2: undefined: config.LoadEnv
The evaluator succeeded (a .drv was written). The Go compiler failed inside the sandbox. The fix belongs in the source code, not in the Nix expression.
Diagnostic rule: if you see a /nix/store/...drv path in the error message, the evaluator succeeded and the problem is in the build. If there is no .drv path, the problem is in the Nix expression itself.
The boring rule
- Open
nix repl .#before editing any file in a flake. Keep it open alongside your editor. - Use
:t exprto sanity-check that a binding holds the type you expect before wiring it into a larger expression. - Use
builtins.traceto confirm which branch a conditional takes and which version of a package is selected. Remove all traces before committing. - Use
:b drvinside the REPL to test builds interactively instead of switching terminals and retypingnix build .#.... - After editing
flake.nix, type:rto reload the flake without restarting the session. - A
.drvpath in the error means eval succeeded; the bug is in source code. No.drvmeans eval failed; the bug is in the Nix expression.
Try this
Start
nix repl 'nixpkgs'. Find theversionandmeta.license.shortNameofpkgs.ripgrep. Then use:e pkgs.ripgrepto open its package definition and read whatbuildInputsit declares.In a flake-based project on your workstation, run
nix repl .#. Type:t devShells.x86_64-linux.defaultto confirm it is a derivation, then:b devShells.x86_64-linux.defaultto build its environment closure. Inspect the output path withnix path-info --closure-size /nix/store/<result>— how large is the closure in megabytes?Write a
probe.nixthat accesses a missing attribute (pkgs.nonexistentTool). Evaluate it withnix-instantiate --eval probe.nix. Then load the same file innix replwith:l probe.nix. Does the error fire on load, or only when you type the binding name and press Enter? What does this tell you about Nix’s lazy evaluation?Add
builtins.tracecalls to an existingshell.nixorflake.nixto print the version of each package in a list usingbuiltins.mapandbuiltins.concatStringsSep. Runnix eval --file shell.nixand confirm the traces appear. Then remove the traces, re-evaluate, and verifystderris clean.