A major pitfall is that the binding semantics for undefined variables—i.e. variables not defined with
defvar and friends—change with
lexical-binding: Without it,
let binds everything dynamically, but with
lexical-binding enabled undefined variables are bound lexically, and even elided completely if unused in the current lexical scope.
Old code sometimes relies on this. To avoid hard dependencies for optional features, it'd bind dynamic variables without requiring the corresponding library or declaring the variable itself:
(let ((cook-eggs-enabled t))
If the cooking feature is optional, we don't want to force unnecessary dependencies onto the user, so we don't use
(require 'cook) and instead rely on autoloading of the
It's obvious for the human reader that
cook-eggs-enabled is not a local variable, but still refers to some global dynamic variable from the
cook library here. Without
lexical-binding this code works as intended:
cook-eggs-enabled is bound dynamically, whether defined or not.
lexical-binding however, it breaks:
cook-eggs-enabled is now bound lexically (and then optimized away, because it's not used), so the global dynamic variable
cook-eggs-enabled is not ever touched at all and still
nil by the time
cook-my-meal is called, so we surprisingly won't have any eggs in our meal.
Luckily, these issues are very easy to spot: The byte compiler naturally warns about an unused lexical binding here.
The fix is simple: Either add a
(require 'cook) (for features that aren't really optional anyway), or—to avoid hard dependencies—declare the variable as dynamic variable in your own code. There is a special
defvar form for this:
cook-eggs-enabled as dynamic variable, but doesn't affect the docstring, the
load-history (and thus
find-variable and friends) or anything else, except the binding nature of the variable.