Emacs Lisp Development Skill
You are assisting with Emacs Lisp development. Your goal is to help the user write correct, idiomatic elisp and debug issues efficiently.
Parenthesis Matching & Navigation
Built-in Commands
| Key / Command | Action |
|---|---|
C-M-f (forward-sexp) |
Move forward across one balanced expression |
C-M-b (backward-sexp) |
Move backward across one balanced expression |
C-M-n (forward-list) |
Move forward across one balanced list |
C-M-p (backward-list) |
Move backward across one balanced list |
C-M-u (backward-up-list) |
Move up one level of parentheses |
C-M-d (down-list) |
Move down one level of parentheses |
C-M-k (kill-sexp) |
Kill the balanced expression following point |
C-M-SPC (mark-sexp) |
Mark the balanced expression following point |
C-M-t (transpose-sexps) |
Transpose the sexps around point |
Showing Matching Parens
Enable show-paren-mode globally:
(show-paren-mode 1)
Or configure it:
(setq show-paren-delay 0
show-paren-style 'mixed) ; 'parenthesis, 'expression, or 'mixed
Structural Editing with paredit or smartparens
Paredit provides strict structural editing:
| Key | Action |
|---|---|
( |
Inserts () and places cursor inside |
) |
Moves past closing paren ("smart close") |
M-(" |
Wraps the next sexp in parentheses |
M-s" |
Splits the current sexp at cursor |
M-r" |
Raises the current sexp (replaces parent with child) |
C-k" |
Kills the sexp, preserving structure |
| `M-`` | Splice sexp, killing everything around it |
| `M-`` | Splice sexp into parent |
Smartparens is more flexible and works with multiple languages:
(require 'smartparens-config)
(smartparens-global-mode 1)
Finding Unbalanced Parentheses
Use
check-parens:M-x check-parensjumps to the first unmatched paren- Works in any mode, not just elisp
Use Emacs to scan:
;; Evaluate this to find the position of unbalanced parens (with-current-buffer (current-buffer) (goto-char (point-min)) (condition-case err (while (not (eobp)) (forward-sexp)) (error (message "Unbalanced at position %d, line %d" (point) (line-number-at-pos)))))Use
count-pairsapproach:;; Count all parens in the file (let ((open (count-matches "(" (point-min) (point-max))) (close (count-matches ")" (point-min) (point-max)))) (message "Open: %d, Close: %d, Diff: %d" open close (- open close)))Narrow to the problematic region:
- Select the region you think is correct
C-x n n(narrow-to-region)M-x check-parensC-x n w(widen) to restore
Common Paren Mistakes
- Missing close paren at end of file: Emacs reports "End of file during parsing"
- Extra close paren: Emacs reports "Scan error: Unbalanced parentheses"
- Mismatched string quotes: A
"inside a string without escaping breaks parsing - Unescaped backticks or commas inside docstrings
Debugging Techniques
1. message — The Print Debugger
(message "Value of x: %S" x) ; %S for lisp objects, %s for strings
(message "Point is at %d" (point))
(message "Buffer: %s, Line: %d" (buffer-name) (line-number-at-pos))
View the *Messages* buffer with C-h e.
2. debug-on-error — Automatic Stack Traces
(setq debug-on-error t) ; Enter debugger on any error
(setq debug-on-signal t) ; Also break on signals (very verbose)
Toggle interactively: M-x toggle-debug-on-error
In the debugger:
c— continued— step through (single-step)q— quit debuggere— evaluate expressionh— help
3. Edebug — The Source-Level Debugger
Instrument a function for debugging:
C-u C-M-x(eval-defunwith prefix) — instrument the current defunM-x edebug-defun— same thingM-x edebug-all-defs— instrument all subsequent defs as they're evaluated
Execution control while in Edebug:
| Key | Action |
|---|---|
SPC |
Step through (next expression) |
n |
Next (skip stepping into function calls) |
t |
Trace (show but don't stop at each step) |
T |
Rapid trace (faster, less display) |
g |
Go (run until next breakpoint) |
c |
Continue (run until done or break) |
f |
Forward (exit current function) |
o |
Out (step out of current function) |
i |
Step in (step into function call) |
q |
Quit Edebug |
b |
Set breakpoint at point |
u |
Unset breakpoint |
x |
Set conditional breakpoint |
e |
Evaluate expression (M-: in Edebug) |
r |
Return a value from current function |
d |
Display details of current expression |
w |
Where (show backtrace) |
? |
Help |
Breakpoints:
bat point to set/unsetxfor conditional:x (equal x 42)Bto show all breakpointsUto unset all breakpoints
Evaluate expressions during debugging:
e— evaluate in minibufferM-:— evaluate with completion
4. trace-function — Function Call Tracing
(require 'trace)
(trace-function 'my-function) ; Trace calls to my-function
(untrace-function 'my-function) ; Stop tracing
(untrace-all) ; Stop all tracing
Output goes to *trace-output* buffer.
5. Backtrace on Demand
(debug) ; Enter debugger immediately when this is evaluated
Or use M-x debug to enter the debugger at the current position.
6. Timing and Performance
;; Simple timing
(benchmark-run 1 (my-function))
;; More detailed
(benchmark-run-compiled 100 (my-function))
;; Profile a specific function
(profiler-start 'cpu)
(my-function)
(profiler-stop)
(profiler-report)
7. Inspecting Variables and State
;; Describe a variable
C-h v variable-name
;; Describe a function
C-h f function-name
;; Apropos (search for symbols)
C-h a regexp
;; Inspect value
M-: variable-name RET
;; Pretty-print a complex structure
M-x pp-eval-expression RET variable-name RET
Development Workflow
Evaluating Code Incrementally
| Key | Action |
|---|---|
C-M-x |
Evaluate the current defun |
C-x C-e |
Evaluate the sexp before point |
C-j |
Evaluate and print result in current buffer (Lisp Interaction mode) |
M-: |
Evaluate expression in minibuffer |
C-x C-e with prefix (C-u) |
Insert result into buffer |
Working with Multiple Files
;; Load a file
(load-file "~/path/to/file.el")
;; Require a feature (triggers autoloads)
(require 'my-feature)
;; Load after another package loads
(with-eval-after-load 'org
(require 'my-org-extension))
Testing Changes
- Edit the function
C-M-xto evaluate it- Test it in a scratch buffer or via
M-: - If it's broken,
C-h eto check messages - If needed,
C-u C-M-xto instrument with Edebug
Byte Compilation
;; Byte-compile current buffer
M-x emacs-lisp-byte-compile
;; Byte-compile and load
M-x emacs-lisp-byte-compile-and-load
;; Check for warnings
M-x byte-compile-file
Always check compiler warnings — they often catch real bugs:
- Unused variables
- Undefined functions
- Obsolete function calls
- Wrong number of arguments
Common Debugging Scenarios
"End of file during parsing"
M-x check-parens— jumps to the problem- Look for missing close parens near the end of the file
- Check for unterminated strings (
"without matching") - Check for unterminated comments (
;is fine, but#|needs|#)
"Symbol's function definition is void"
- The function hasn't been defined or loaded
- Check if you need
(require 'some-library) - Check for typos in the function name
- If it's a macro, ensure the file is loaded before the macro is used
"Wrong number of arguments"
- Check the function signature with
C-h f function-name - Some functions changed signatures between Emacs versions
- Check if a macro is being called like a function
"Args out of range" or "Beginning/End of buffer"
- Check buffer boundaries before operations
- Use
save-excursionorsave-restrictionif modifying point
"Invalid function"
- Usually means a list is being called as a function
- Check if you meant to use
funcallorapply - Often caused by missing
#'before function names
Infinite Loop or Hanging
C-gto quitM-x toggle-debug-on-quitto get a backtrace onC-g- Add
(message "…")calls to find the loop - Check
whileloops for missing increment/progress
Useful Minor Modes
;; Enable eldoc (show function signature in echo area)
(add-hook 'emacs-lisp-mode-hook #'eldoc-mode)
;; Enable flymake for on-the-fly checking
(add-hook 'emacs-lisp-mode-hook #'flymake-mode)
;; Enable paredit or smartparens
(add-hook 'emacs-lisp-mode-hook #'paredit-mode)
;; or
(add-hook 'emacs-lisp-mode-hook #'smartparens-strict-mode)
;; Enable rainbow delimiters for visual paren matching
(add-hook 'emacs-lisp-mode-hook #'rainbow-delimiters-mode)
Best Practices
- Always use
lexical-binding: tin the file header - Use
#'(function quote) when passing functions:(mapcar #'car list) - Use
defcustomfor user-facing variables,defvarfor internal - Prefix all symbols with your package name:
my-package--private,my-package-public - Write docstrings for every public function and variable
- Use
when/unlessinstead of(if … nil) - Use
condition-casefor operations that might fail - Use
with-temp-bufferinstead of creating and managing temp buffers manually - Check for nil before calling
car/cdron potentially empty lists - Test incrementally — evaluate each function as you write it
Quick Reference: Debugging Commands
C-M-x eval-defun
C-u C-M-x eval-defun + edebug instrument
C-x C-e eval-last-sexp
M-: eval-expression
C-h e view-echo-area-messages
M-x edebug-defun
M-x toggle-debug-on-error
M-x check-parens
C-M-f forward-sexp
C-M-b backward-sexp
C-M-u backward-up-list
C-M-d down-list