Practical Nix Language for Daily Use
This section focuses on how the Nix language is actually used in daily development:
- reading existing code
- debugging evaluation errors
- composing configurations
- working effectively with nixpkgs
The Nix language itself is small; the skill is learning how to use it fluently.
1. Reading and Navigating Existing Nix Code
Section titled “1. Reading and Navigating Existing Nix Code”Most Nix you interact with already exists. Your job is usually to:
- understand what shape a value has
- find where attributes come from
- determine where overrides apply
Practical tips
Section titled “Practical tips”- Start by identifying the outermost attrset
- Look for
letbindings to see how values are assembled - Ignore implementation details at first — focus on attribute names
Example:
let cfg = { enable = true; port = 8080; };in{ service = cfg;}Focus first on:
- What attributes exist?
- What is the final shape?
Exercise: Reading code
Section titled “Exercise: Reading code”-
Open a Nix file from nixpkgs (any derivation).
-
Identify:
- What arguments the file takes
- What attrset it returns
-
Ignore build logic — just list the attribute names you see.
2. Documentation: Where to Look (and How)
Section titled “2. Documentation: Where to Look (and How)”Nix documentation is distributed and uneven — this is normal.
Primary sources
Section titled “Primary sources”- Nix language manual
- nixpkgs manual
- Source code (often the most accurate)
Practical workflow
Section titled “Practical workflow”-
Use
rg(ripgrep) to search nixpkgs:Terminal window rg mkDerivationrg callPackagerg mkIf -
Jump from usage → definition
-
Read examples before reading prose
Exercise: Finding documentation
Section titled “Exercise: Finding documentation”- Search for
lib.mkIfin nixpkgs. - Find its definition.
- Write a one-sentence description of what it does.
3. Tracing, Debugging, and Inspecting Values
Section titled “3. Tracing, Debugging, and Inspecting Values”Debugging is unavoidable in Nix.
builtins.trace
Section titled “builtins.trace”builtins.trace "hello" valueThis prints during evaluation when the value is forced.
Showing stack traces
Section titled “Showing stack traces”nix eval --show-trace ...Always use this when debugging.
REPL inspection
Section titled “REPL inspection”nix repl:l <nixpkgs>:p pkgs.helloExercise: Tracing evaluation
Section titled “Exercise: Tracing evaluation”let x = builtins.trace "evaluating x" 1; y = 2;iny- Evaluate this expression.
- Observe that the trace does not print.
- Change the expression so
xis forced.
4. Laziness: Practical Consequences
Section titled “4. Laziness: Practical Consequences”Nix evaluates lazily. This affects:
- debugging
- conditionals
- error visibility
Example:
if false then throw "boom" else 42This evaluates to 42.
Forcing Evaluation: seq vs deepSeq
Section titled “Forcing Evaluation: seq vs deepSeq”Because Nix is lazy, values are not evaluated until needed. Sometimes you want to force evaluation deliberately, usually for:
debugging
- surfacing errors early
- ensuring a value is “fully realized”
- Nix provides two related tools for this:
builtins.seqbuiltins.deepSeqbuiltins.seq: force just enough
builtins.seq a bmeans:
Evaluate a to weak head normal form, then return b.
In practice:
- Nix evaluates a only far enough to know what it is
- It does not recursively evaluate inside attrsets or lists
- Example: seq does not force inside attrsets
let x = { a = throw "boom"; };inbuiltins.seq x 42✅ This evaluates to:
42Why?
- x is an attrset
- seq only needs to know “this is an attrset”
- It does not evaluate
x.a
Example: seq does force top-level expressions
builtins.seq (throw "boom") 42❌ This throws:
error: boomBecause:
- The expression itself must be evaluated
- There’s no structure to defer inside
builtins.deepSeq: force everything
builtins.deepSeq a bmeans:
Fully evaluate a, recursively, then return b.
This walks:
- attrsets
- lists
- nested structures
- and forces all values inside them.
Example: deepSeq forces nested values
let x = { a = throw "boom";};inbuiltins.deepSeq x 42❌ This throws:
error: boomBecause:
deepSeqdescends into x- Forces
x.a
Example: lists and laziness
let xs = [ 1 (throw "boom") 3 ];inbuiltins.seq xs 42✅ Returns:
42
But:
builtins.deepSeq xs 42❌ Throws:
error: boomAgain:
seq only checks “this is a list”
deepSeq evaluates every element
Each solves a different practical problem.
Use seq when you want to:
- force that something exists
- ensure a value is not bottom (throw, infinite recursion)
- trigger a trace at a specific point
- minimally disturb laziness
Use deepSeq when you want to:
- ensure a structure is fully valid
- surface errors hidden deep inside configs
- debug complex nested attrsets
- “validate” computed values
Exercise: Laziness in action
Section titled “Exercise: Laziness in action”- Create a value that throws.
- Reference it in an unused branch.
- Force it using
seq.
5. Attrsets as APIs and Interfaces
Section titled “5. Attrsets as APIs and Interfaces”Attrsets define contracts.
Defensive access
Section titled “Defensive access”cfg.enable or falsecfg ? enableDefaults
Section titled “Defaults”{ enable ? false, ... }: enableExercise: Designing an interface
Section titled “Exercise: Designing an interface”- Write a function that accepts
{ enable, port ? 8080 }. - Return an attrset describing a service.
- Call it with and without
port.
6. Functions as Configuration Transformers
Section titled “6. Functions as Configuration Transformers”Functions are often used to transform attrsets.
cfg: cfg // { debug = true; }This pattern underlies:
- overlays
- modules
- overrides
Exercise: Layering config
Section titled “Exercise: Layering config”- Define a base config attrset.
- Write a function that adds logging.
- Apply it to the base config.
7. Evaluation vs Build Time
Section titled “7. Evaluation vs Build Time”This distinction explains many errors.
Evaluation time
Section titled “Evaluation time”- Running Nix expressions
- Computing values
- No side effects
Build time
Section titled “Build time”- Happens in a sandbox
- Happens after evaluation
- Can run shell commands
Exercise: Spot the phase
Section titled “Exercise: Spot the phase”For each expression, decide:
- evaluation time or build time?
builtins.readFile ./file.txtpkgs.stdenv.mkDerivation { buildPhase = "cat file.txt";}8. Paths, Strings, and the Store
Section titled “8. Paths, Strings, and the Store”Paths are tracked inputs. Strings are not.
Correct
Section titled “Correct”builtins.readFile ./config.txtIncorrect
Section titled “Incorrect”builtins.readFile "./config.txt"Exercise: Path handling
Section titled “Exercise: Path handling”- Define a path using
./. - Convert it to a string via interpolation.
- Try to use it as a path again and observe the failure.
9. Working with lib
Section titled “9. Working with lib”The lib attrset contains helpers you will use daily.
Common ones:
lib.optionallib.optionalslib.mkIflib.attrsetslib.lists
Example:
lib.optional cfg.enable pkgs.helloExercise: Using lib.optional
Section titled “Exercise: Using lib.optional”- Create a boolean flag.
- Conditionally include a package in a list.
- Toggle the flag and observe the result.
10. Overrides and Overlays
Section titled “10. Overrides and Overlays”override vs overrideAttrs
Section titled “override vs overrideAttrs”override→ function argumentsoverrideAttrs→ derivation attributes
Example:
self: super: { hello = super.hello.overrideAttrs (_: { pname = "hello-custom"; });}Exercise: Applying an overlay
Section titled “Exercise: Applying an overlay”- Create an overlay that renames hello.
- Import nixpkgs with this overlay enabled.
- Evaluate the package and confirm the name change.
11. Reading Error Messages
Section titled “11. Reading Error Messages”Common errors:
attribute 'x' missingattempt to call something which is not a function- infinite recursion
Strategy
Section titled “Strategy”- Read the first error
- Use
--show-trace - Look for the shape mismatch
Exercise: Break things on purpose
Section titled “Exercise: Break things on purpose”- Call an attrset like a function.
- Access a missing attribute.
- Practice reading the error messages.
12. Refactoring Nix Code Safely
Section titled “12. Refactoring Nix Code Safely”Refactoring usually means:
- introducing
let - extracting functions
- reducing duplication
Example
Section titled “Example”Before:
{ pkgs }: { a = pkgs.hello; b = pkgs.hello;}After:
{ pkgs }:let hello = pkgs.hello;in{ a = hello; b = hello;}Exercise: Refactor
Section titled “Exercise: Refactor”Extract a “makeService” function
This is very common in NixOS module code: same attrset structure repeated.
let web = { enable = true; name = "web"; port = 8080; env = { LOG_LEVEL = "info"; REGION = "us-west"; }; };
worker = { enable = true; name = "worker"; port = 9090; env = { LOG_LEVEL = "info"; REGION = "us-west"; }; };in{ services = [ web worker ];}Goal
Refactor so: - the repeated env block is defined once - the repeated “service shape” is built by a function - you can create a third service with minimal repetition
Constraints
- Keep the final output shape the same ({ services = [ … ]; })
- Don’t remove fields; only refactor
Make a function like:
mkService = { name, port }: { ... };