Emacs Lisp Lexical Binding: Why Your Closures Capture Values, Not Names
Emacs Lisp defaults to dynamic scoping, causing closures to capture variable names, not values. Adding a file-local variable enables lexical binding per file, fixing callback bugs and letting the byte compiler optimize. Includes a timer-loop example, macro trade-offs, and verification steps.
05 Jan 2026, 22:06 UTC

The Problem: Dynamic Scope Surprises
Emacs Lisp defaults to dynamic scoping. When a function references a free variable, the interpreter walks the runtime call stack to find the most recent binding. This behavior causes subtle bugs in callbacks, advice, and asynchronous code: a closure captures the variable name, not its value at definition time. If the caller later rebinds that name, the closure sees the new value.
Consider a timer callback that captures a loop variable. Under dynamic scope, all callbacks see the final loop value because they share the same variable binding. This is a classic source of off-by-one errors in Emacs packages.
Enabling Lexical Binding Per File
Since Emacs 24.1, adding a file-local variable on the first line switches the entire file to lexical scoping:
;; -*- lexical-binding: t; -*-
The comment must appear on line one (or in a local-variables block at the end). Setting lexical-binding with setq at runtime does not affect already-loaded code; the byte compiler decides the scoping rule when it reads the source.
With lexical binding, a closure captures the value of each lexical variable at the moment the closure is created. The compiler can also optimize variable access, producing faster and smaller bytecode.
Worked Example: Timer Callbacks in a Loop
Create a file lexical-demo.el with the first line above. Then define:
(defun lexical-demo-timers ()
(dotimes (i 3)
(run-at-time (* i 2) nil
(lambda () (message "Timer %d fired" i)))))
Byte-compile the file (M-x byte-compile-file) and load it. Call M-x lexical-demo-timers. You will see three messages, each printing Timer 0 fired, Timer 1 fired, Timer 2 fired — each closure captured the value of i at its iteration.
Now remove the file-local variable, recompile, and reload. The same call prints Timer 3 fired three times because the dynamic i ends at 3 and all closures share that single binding.
Trade-offs and Limitations
- Macro compatibility: Macros that expect free variables to be resolved in the caller's environment (common in older code) will break under lexical binding. You must either rewrite the macro or keep that file dynamic.
- Mixed scopes in one session: Emacs supports both scoping rules simultaneously. However, a function defined in a dynamic file called from lexical code still uses dynamic lookup for its free variables. This asymmetry can cause unexpected captures when crossing file boundaries.
- Init file conversion risk: Switching your
init.elto lexical binding may break third-party packages or personal functions that rely on dynamic variables likecurrent-prefix-argor package-specific globals. Test incrementally. - Version requirement: Emacs releases before 24.1 ignore the file variable and treat the file as dynamic. Guard with
(when (>= emacs-major-version 24) ...)if you support older versions.
Verifying the Setting
In a buffer visiting a lexically bound file, evaluate:
(symbol-value 'lexical-binding) ; => t
Run M-x describe-variable RET lexical-binding RET to read the built-in documentation and confirm the default for your Emacs version. Check emacs --version to ensure you are on 24.1 or later.
Actionable Next Step
Pick one personal library file that defines callbacks or advice. Add the file-local variable, byte-compile, and run its test suite (or exercise the commands manually). If something breaks, you have found a dynamic-scope dependency — either refactor that dependency or keep the file dynamic. Gradually migrate files where the benefit outweighs the risk.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.