Using Emacs lexical-binding to avoid accidental variable capture
Learn how to enable Emacs’ lexical-binding feature to prevent accidental variable capture, with a concrete example and verification steps.
25 Sept 2026, 19:35 UTC

The problem: unexpected variable capture in Emacs Lisp
When you write Emacs Lisp code that defines a function inside a let block, you might expect the function to see only the variables bound in that block. With Emacs’ default dynamic scoping, however, the function can also see any variable that happens to be bound at runtime, leading to subtle bugs when a caller rebinds a name you thought was private.
Thesis: enable lexical-binding per file for safer, more predictable scoping
By adding the file‑local variable lexical-binding: t to the first line of an Emacs Lisp source file, you tell Emacs to evaluate that file using lexical scoping. Free variables are then resolved from the static environment where the function was defined, not from the runtime call stack. This change is backward‑compatible: files without the flag continue to use dynamic scope, allowing a gradual migration.
How to enable and verify lexical-binding
- Open (or create) a file, e.g.
~/my‑test.el. - On the very first line, add the header:
;; -*- lexical-binding: t; -*- - Save the file and re‑evaluate the buffer (M-x revert-buffer or C-x C-v).
- Check that the variable is active:
should showC-h v lexical-binding RETtfor the current buffer. - Run a quick test in the same buffer:
You should seeM-x eval-expression RET (let ((x 42)) (defun my‑get‑x () x) (my‑get‑x))42in the minibuffer. If you remove the header and repeat the expression, Emacs will signal avoid-variableerror becausexis not dynamically visible.
Worked example: fixing a capture bug
Suppose you have a utility that creates a temporary counter:
(defun make-counter ()
(let ((count 0))
(lambda ()
(setq count (1+ count))
count)))
With dynamic scoping, if a caller later executes (let ((count 99)) (funcall (make-counter))), the inner lambda sees the caller’s count and returns 100 instead of 1. Adding lexical-binding: t to the file makes the lambda close over the count from its defining let, so the caller’s binding does not affect it.
Trade‑offs and limitations
- Existing code that relies on dynamic behavior—such as
defvarspecial variables,setqin hook functions, or code that expects variables to be visible viasymbol-value—may break when the file is switched to lexical scope. - Third‑party packages that lack the header will continue to use dynamic scope, which is fine as long as they are not loaded into a lexical buffer where they inadvertently capture locals. Testing each package in a lexical buffer is recommended.
- The header only affects the file in which it appears; you cannot enable lexical‑binding for a single function within a dynamically scoped file without moving that function to its own file or using
lexical-letfrom thelexical-bindinglibrary (available in newer Emacs versions).
Actionable closing
Start by adding the lexical-binding: t header to any new Emacs Lisp file you write. For existing files, enable the header in a test buffer, run your usual workflow, and verify that functions behave as expected. If you encounter failures, examine whether the code depends on dynamic variable visibility and consider refactoring those parts to use explicit arguments or defvar with a * naming convention. This incremental approach lets you enjoy the safety of lexical scoping without a disruptive migration.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.