The Nix REPL and Debugging Expressions

Updated

September 12, 2026

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 directory

Nix 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 repl

Output:

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

Output:

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

Output:

«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 name

Output:

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
  step3

Evaluate:

nix eval --file default.nix

Output:

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.typo
error: 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-service
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

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 expr to sanity-check that a binding holds the type you expect before wiring it into a larger expression.
  • Use builtins.trace to confirm which branch a conditional takes and which version of a package is selected. Remove all traces before committing.
  • Use :b drv inside the REPL to test builds interactively instead of switching terminals and retyping nix build .#....
  • After editing flake.nix, type :r to reload the flake without restarting the session.
  • A .drv path in the error means eval succeeded; the bug is in source code. No .drv means eval failed; the bug is in the Nix expression.

Try this

  1. Start nix repl 'nixpkgs'. Find the version and meta.license.shortName of pkgs.ripgrep. Then use :e pkgs.ripgrep to open its package definition and read what buildInputs it declares.

  2. In a flake-based project on your workstation, run nix repl .#. Type :t devShells.x86_64-linux.default to confirm it is a derivation, then :b devShells.x86_64-linux.default to build its environment closure. Inspect the output path with nix path-info --closure-size /nix/store/<result> — how large is the closure in megabytes?

  3. Write a probe.nix that accesses a missing attribute (pkgs.nonexistentTool). Evaluate it with nix-instantiate --eval probe.nix. Then load the same file in nix repl with :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?

  4. Add builtins.trace calls to an existing shell.nix or flake.nix to print the version of each package in a list using builtins.map and builtins.concatStringsSep. Run nix eval --file shell.nix and confirm the traces appear. Then remove the traces, re-evaluate, and verify stderr is clean.