What Is Emacs Debugging?
Emacs debugging refers to the suite of tools and techniques available within Emacs for inspecting, tracing, and fixing errors in code. This includes debugging Emacs Lisp (Elisp) programsโthe language that powers Emacs itselfโas well as debugging external programs such as C, Python, or JavaScript directly from within the editor using integrated debuggers like GDB. Emacs provides multiple debugging layers: a simple interactive debugger (debug.el), a powerful source-level instrumenting debugger (edebug), a backtrace inspector, and full front-ends for external debugging tools.
At its core, Emacs debugging is about gaining visibility into the execution flow of code. Whether you are developing an Emacs package, hacking on Emacs's C core, or using Emacs as an IDE for another language, understanding how to pause execution, inspect variables, step through forms, and analyze stack traces is essential.
Why Debugging in Emacs Matters
๐ Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Emacs is not just a text editorโit is a programmable, self-documenting Lisp machine. The vast majority of Emacs functionality is implemented in Elisp, and users constantly extend it. When your .emacs or init.el throws an obscure error, or a third-party package behaves unexpectedly, knowing how to debug saves hours of frustration. Beyond Elisp, Emacs offers a unified debugging interface for compiled languages via GDB, allowing you to set breakpoints, watch expressions, and navigate stack frames without leaving the editor. This tight integration means you can maintain flow and context, drastically reducing the time from bug discovery to fix.
The Built-in Debugger: debug.el
The most basic and essential debugging tool in Emacs is the built-in debugger, invoked by the function debug or automatically when an error is signaled. It provides a backtrace of the call stack and lets you evaluate expressions in the context of any frame.
Entering the Debugger
You can enter the debugger in several ways:
- On error: Set
debug-on-errorto non-nil to drop into the debugger whenever an error occurs. - On a specific error: Customize
debug-ignored-errorsanddebug-on-errorto filter which errors trigger it. - On a function call: Set
debug-on-entryfor a specific function name. - Explicitly: Insert
(debug)calls into your code.
Using debug-on-error
The simplest way to start debugging is to toggle debug-on-error:
;; Toggle debug-on-error
M-x set-variable RET debug-on-error RET t RET
Or in your init file:
(setq debug-on-error t)
Now, whenever any unhandled error occurs, Emacs will open a *Backtrace* buffer showing the call stack, the error message, and a prompt for debugger commands.
Navigating the Backtrace
When the debugger pops up, you see something like this:
Debugger entered--Lisp error: (void-function my-undefined-function)
(my-undefined-function)
(my-wrapper-function)
(progn (my-wrapper-function) (message "Done"))
eval((progn (my-wrapper-function) (message "Done")) :eval-exp-...)
eval-expression((progn (my-wrapper-function) (message "Done")) nil nil 127)
funcall-interactively(eval-expression ...)
#<subr call-interactively>(eval-expression nil nil)
apply(#<subr call-interactively> eval-expression (nil nil))
...
In the backtrace buffer, you can use these keys:
- c โ Continue execution (if possible).
- e โ Evaluate an expression in the context of the current frame.
- r โ Reevaluate the expression that caused the error (useful after fixing it on the fly).
- d โ Step through the debugger itself (meta-debugging!).
- q โ Quit the debugger and return to top-level.
- ? โ Show all debugger commands.
Evaluating in Frame Context
Press e in the backtrace buffer to evaluate an expression in the scope of that stack frame. This lets you inspect local variables, test fixes, and understand the state at the time of the error:
;; Press 'e' on a frame, then type:
(current-buffer)
;; or
(buffer-name)
;; or just a variable name
my-local-var
The result appears in the echo area or minibuffer.
debug-on-entry for Specific Functions
To debug a single function without triggering on all errors, use debug-on-entry:
(debug-on-entry 'my-function-name)
Now whenever my-function-name is called, the debugger opens. You can step through its execution. To disable it:
(cancel-debug-on-entry 'my-function-name)
Edebug: The Source-Level Instrumenting Debugger
While debug.el gives you a post-mortem backtrace, Edebug lets you step through Elisp code line-by-line, before an error occurs. Edebug instruments your source code, inserting hooks between expressions so you can watch execution unfold in real time.
Instrumenting a Function for Edebug
To instrument a function, move point into its definition and press C-u C-M-x (which calls edebug-defun with a prefix argument). Alternatively, use:
M-x edebug-defun RET
The function's source code is now "instrumented" โ marked with dots in the fringe or highlighted. When you invoke the function, Edebug takes control.
Edebug Commands
Once execution hits the instrumented function, an *edebug* buffer appears showing the source with an arrow indicating the current expression. You control execution with these single-key commands:
- SPC (space) โ Step through the next expression.
- n โ Step over to the next expression (skip details of sub-expressions).
- i โ Step into a function call (if the called function is also instrumented).
- o โ Step out of the current function, finishing it and returning to the caller.
- b โ Set a breakpoint at point.
- x โ Set a conditional breakpoint (break only when condition evaluates to non-nil).
- e โ Evaluate an expression in the current lexical environment.
- q โ Quit Edebug, aborting execution.
- G โ Resume execution until the next breakpoint.
- h โ Proceed to here (run until point).
- ? โ Display help.
Practical Edebug Example
Consider this function in your init file:
(defun my-calculate-fibonacci (n)
"Return the Nth Fibonacci number."
(let ((a 0)
(b 1))
(while (> n 0)
(let ((next (+ a b)))
(setq a b
b next)
(setq n (1- n))))
a))
To debug it, place point inside the defun, press C-u C-M-x, then evaluate:
M-: (my-calculate-fibonacci 10) RET
Edebug pauses at the first expression. Press SPC repeatedly to watch a, b, and n change. Press e at any point and type a or b to inspect values. Use b to set breakpoints on specific lines, then G to run until that breakpoint fires.
Edebug and Macros
Edebug can instrument macros too. Use edebug-eval-top-level-form or simply C-u C-M-x on the macro definition. When you later expand and evaluate a call to that macro, Edebug steps through the expansion process, showing you exactly how the macro transforms at each stage.
Debugging with GDB in Emacs: gdb-mi Mode
Emacs includes a powerful GDB front-end called gdb-mi (GDB Machine Interface). This mode provides multiple windows for source code, breakpoints, stack frames, local variables, and I/O, all synchronized within the Emacs frame.
Starting a GDB Session
To debug a C or C++ program (or any GDB-supported language), use:
M-x gdb RET
;; Then enter the gdb command, e.g.:
gdb --i=mi myprogram RET
Alternatively, if your program needs arguments:
gdb --i=mi --args ./myprogram arg1 arg2
Emacs opens several windows: a GUD interaction buffer (where you type GDB commands), a source buffer, a breakpoints buffer, a stack buffer, and a locals buffer. This is a full debugging workspace.
Key Bindings in GDB Mode
In the source buffer (when execution is paused), you can use:
- C-c C-n โ Next line (step over).
- C-c C-s โ Step into function.
- C-c C-f โ Finish current function (step out).
- C-c C-u โ Continue execution.
- C-x C-a C-b โ Toggle breakpoint at point (fringe indicator appears).
- C-c C-p โ Print expression at point (hover-like inspection).
Setting Breakpoints Visually
Navigate to a line in the source buffer and press C-x C-a C-b (or click in the fringe). A red dot appears. Right-click the fringe dot to make it conditional or temporary. All breakpoints appear in the dedicated *Breakpoints* buffer, where you can enable, disable, or delete them.
Watch Expressions and Locals
The *Locals* buffer automatically displays local variables with their values, updating at each step. To add custom watch expressions, use the GDB command:
(gdb) display myvar
(gdb) display *ptr
(gdb) display/x myflags
These persist across sessions if saved in a .gdbinit file.
Thread and Frame Navigation
For multi-threaded programs, Emacs shows a thread list. Use M-x gdb-display-threads-buffer to see all threads. Click any thread to switch context. The stack buffer shows frames; click any frame to jump to its source location and inspect its locals.
Debugging Elisp with trace-function and message
Not every debugging session requires a full debugger. Emacs provides lightweight tracing facilities that are often faster for isolating problems.
Using trace-function
Trace a function to log every call with its arguments and return value:
(require 'trace)
(trace-function 'my-suspicious-function)
Now invoke the function. Output goes to the buffer *trace-output*:
======================================================================
1 -> (my-suspicious-function arg1 arg2)
1 <- my-suspicious-function: t
======================================================================
2 -> (my-suspicious-function nil "oops")
2 <- my-suspicious-function: nil
To stop tracing:
(untrace-function 'my-suspicious-function)
;; or untrace all:
(untrace-all)
Strategic message Insertion
Sometimes a well-placed message call is the quickest debug tool:
(defun my-buggy-function (x)
(message "DEBUG: my-buggy-function called with x=%S" x)
(let ((result (* x x)))
(message "DEBUG: intermediate result=%S" result)
(1+ result)))
Output appears in the *Messages* buffer. This approach is simple, always available, and doesn't require instrumenting code.
Debugging External Processes and Shell Commands
Emacs can debug not just code but also the processes it spawns. When using M-x compile, M-x shell, or M-x rgrep, problems may arise from environment variables, working directories, or signal handling.
Inspecting the Process Environment
Check what Emacs passes to subprocesses:
(message "PATH=%s" (getenv "PATH"))
(message "PWD=%s" default-directory)
(message "Shell: %s" shell-file-name)
Verbose Compilation Output
When a compile command fails silently, enable verbose debugging:
(setq compile-command "make VERBOSE=1")
;; or for cmake:
(setq compile-command "cmake --build . --verbose")
Then M-x recompile to see detailed output in the *compilation* buffer.
Profiling and Performance Debugging
Sometimes bugs are not crashes but performance issues. Emacs includes a built-in profiler.
Using the Elisp Profiler
Start profiling with:
M-x profiler-start RET
;; Choose: cpu, memory, or both
Perform the slow operation, then:
M-x profiler-report RET
This opens a buffer showing which functions consumed the most CPU time or memory allocations. Expand entries with TAB to drill down. Stop profiling with:
M-x profiler-stop RET
Benchmarking Elisp Expressions
For quick micro-benchmarks:
(benchmark-run 1000
(my-expensive-function))
Returns a list: (total-time gcs gc-time). Use this to compare alternative implementations.
Debugging Emacs Itself
If Emacs crashes or hangs, you can debug the C core. Build Emacs with debugging symbols:
./configure --enable-checking --with-cairo CFLAGS="-g3 -O0"
make
Then run Emacs under GDB from a terminal:
gdb ./src/emacs
(gdb) run
When a segfault occurs, get a backtrace:
(gdb) bt full
This shows the full C call stack with local variables. You can also attach GDB to a running Emacs:
gdb -p $(pidof emacs)
From within Emacs, you can even debug Emacs with itself using M-x gdb and attaching to the parent processโmeta-debugging at its finest.
Best Practices for Emacs Debugging
Effective debugging in Emacs is both a mindset and a set of habits. Here are distilled best practices gathered from seasoned Emacs developers.
1. Always Enable debug-on-error During Development
Keep debug-on-error set to t in your development environment. It transforms cryptic error messages into actionable backtraces:
;; In your init file, conditionally:
(when (not (member "production" (list "production")))
(setq debug-on-error t))
2. Instrument Selectively with Edebug
Don't instrument everything at once. Use C-u C-M-x on one function at a time. If you instrument a widely-called function (like a completion filter), you'll be flooded with Edebug prompts. Use edebug-remove-instrumentation to clean up:
M-x edebug-remove-instrumentation RET
3. Use Conditional Breakpoints
In both Edebug and GDB, set conditions on breakpoints to narrow down intermittent bugs:
;; In Edebug, press 'x' on a breakpoint and enter:
(> n 1000)
;; In GDB:
(gdb) break foo.c:42 if i > 1000
4. Leverage the Backtrace Format
Customize debugger-bury-or-kill and backtrace presentation:
(setq debugger-bury-or-kill 'bury
debugger-stack-frame-format '((" at " . (function . fn)))
debugger-buffer-visible-p t)
5. Keep a Debugging Snippet File
Maintain a personal debugging toolkit in a file like ~/.emacs.d/debug-tools.el:
;; Quick profiling snippet
(defun my-profile-function (fn &optional n)
(interactive "aFunction to profile: \nnNumber of runs: ")
(profiler-start 'cpu)
(dotimes (i (or n 100)) (funcall fn))
(profiler-report)
(profiler-stop))
;; Quick trace snippet
(defun my-toggle-trace (fn)
(interactive "aFunction to trace: ")
(if (get fn 'traced)
(untrace-function fn)
(trace-function fn)))
6. Master the edebug Eval Key
The e key in Edebug evaluates any expression in the current lexical context. Use it to test fixes without restarting:
;; Press 'e', then type:
(setq my-local-var (correct-value my-local-var))
;; Then press 'G' to continue with the corrected state.
7. Use GDB Dashboard Efficiently
Customize the GDB window layout to suit your workflow. Save window configurations with winner-mode or eyebrowse. Keep the locals buffer visible; it eliminates the need for constant print commands.
8. Read the *Messages* Buffer Regularly
Many Emacs bugs leave clues in the *Messages* buffer. Get in the habit of switching to it (C-h e or view-echo-area-messages) after unexpected behavior. It contains message output, warnings, and error summaries.
9. Reproduce with emacs -Q
Before deep debugging, verify the bug exists without your configuration:
emacs -Q --eval "(progn (require 'package-of-interest) (reproduce-bug))"
This isolates whether the problem is in your setup or the package itself.
10. Write Defensive Elisp
Prevent bugs by validating inputs early:
(defun my-safe-function (arg)
(unless (and (integerp arg) (> arg 0))
(error "my-safe-function: ARG must be a positive integer, got %S" arg))
;; ... rest of function
)
Use condition-case to handle expected errors gracefully rather than crashing to the debugger every time.
Conclusion
Emacs offers one of the richest debugging environments in the software world. From the instant feedback of debug-on-error and the surgical precision of Edebug's step-by-step execution, to the industrial-strength GDB integration and lightweight tracing tools, there is a tool for every debugging need. The key to proficiency is practice: instrument functions fearlessly, read backtraces carefully, and cultivate the habit of inspecting state with the evaluation commands. Whether you are untangling a labyrinthine Elisp macro, tracking down a segfault in C, or simply wondering why your configuration behaves oddly, Emacs's debugging arsenal transforms opaque failures into transparent, fixable events. Embrace these tools, and you will spend less time guessing and more time building.
๐ Need a reliable AI agent for your project?
Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.
Get Started โ $23.99/mo