Emacs Introspection and Debugging
If you use Emacs, you will eventually run into errors. Maybe a recent package update introduced some new issues with your system. Or some custom elisp you wrote runs into an edge case. Regardless of the cause, Emacs provides many tools for identifying the causes of errors and learning how to address them.
This post works through a recent issue I encountered with the eglot
package and how I was able to identify and fix the issue using various introspection tools built into Emacs. It provides some general advice for how to use Emacs to learn about and debug issues with Emacs.
Introduction
For a while, whenever I used eglot (mostly for Python projects) and then shut it down with e.g. M-x eglot-shutdown
, all subsequent attempts to save any files in Emacs would return the following error: (jsonrpc-error-message . "No current JSON-RPC connection")
. The save would generally succeed, so this was more annoying than anything. But it was annoying.
Some quick searching online did not yield any useful discussions of how to fix the issue. And neither Claude nor Perplexity had much to offer, either. So I decided to use this as an excuse to learn more about built-in Emacs introspection and debugging tools.
I went in with the following information:
- The issue was related to
eglot
. Even though the error message did not reference eglot, the issue consistently appeared after using eglot and then shutting it down. - The issue was triggered when I saved files.
Given this pattern, I started by looking for hooks related to saving.
Finding Relevant Variables
I wasn't entirely sure what I was looking for. But I knew from previous reading that hooks are variables that, by convention, include the word hook
in their names. This already provides plenty to work with.
The apropos-variable
function takes a pattern or a list of words and returns an *Apropos*
buffer with a list of matching variables. In this case, I interactively used apropos-variable
with M-x apropos-variable RET save hook
to search for variables matching save
and hook
. This returned a couple of good candidates for further exploration!

The after-save-hook
and before-save-hook
variables look especially promising. We can inspect those further for anything eglot
-related by selecting them from the *Apropos*
buffer or searching for them with C-h v <variable-name>
. Following this approach showed me that:
- The value of
before-save-hook
isnil
. - The value of
after-save-hook
is(eglot-format)
.
This is already very useful and points toward some directions for fixing the issue! after-save-hook
is a "Normal hook that is run after a buffer is saved to its file." This would explain why (1) the issue is associated with saving files and (2) the saves are successful in spite of the error (the hook is run after the save, not before).
Confirming the issue with error backtrace
I wanted to confirm that this hook was causing the issue so I used M-x toggle-debug-on-error
to get a more detailed backtrace. In reality, this is where I should have started—the error trace provides much more specific and useful information than the short error message returned in the echo area. When I tried to save a file with debugging enabled, I received the following in a Backtrace buffer.
Debugger entered--Lisp error: (jsonrpc-error "No current JSON-RPC connection" (jsonrpc-error-code . -32603) (jsonrpc-error-message . "No current JSON-RPC connection")) jsonrpc-error("No current JSON-RPC connection") eglot--current-server-or-lose() eglot-server-capable(:documentFormattingProvider) eglot-server-capable-or-lose(:documentFormattingProvider) eglot-format() run-hooks(after-save-hook) #<subr basic-save-buffer>(t) polymode-with-current-base-buffer(#<subr basic-save-buffer> t) apply(polymode-with-current-base-buffer #<subr basic-save-buffer> t) basic-save-buffer(t) save-buffer(1) funcall-interactively(save-buffer 1) command-execute(save-buffer)
You can read this backtrace from bottom to top. After saving the buffer, we see that run-hooks(after-save-hook)
runs, which results in eglot-format()
being run. The final function called before the error is eglot--current-server-or-lose()
. Inspecting this with C-h f RET eglot--current-server-or-lose
tells us that this function returns the "current logical Eglot server connection or error." If I'm saving some random file that is not going to use a Python LSP server, we would expect this to return an error.
Now that we have some understanding of what is happening, how do we fix it?
Quick fix—bandaid approach
My initial fix for this issue was to write a simple cleanup script to remove the offending hook function from the after-save-hook
.
(use-package eglot :straight (:type built-in) :hook ((python-mode . eglot-ensure)) :config (setq eglot-autoshutdown t) (defun my-eglot-shutdown-cleanup (&rest _) "Perform thorough cleanup after Eglot shutdown." (remove-hook 'after-save-hook #'eglot-format nil) (advice-add 'eglot-shutdown :after #'my-eglot-shutdown-cleanup))
This isn't perfect. It does prevent saving from returning an error after I've shut down eglot, resolving a significant nuisance. However, the issue remains when eglot is running and I try to save a buffer without an associated LSP server; i.e., if I am using eglot in a Python buffer but then try to save an org buffer. The eglot-format
hook function is still active; there is no running language server to provide formatting for org buffers, so the hook function returns an error.
At this point, it was not yet clear to me how the hook was set in the first place. Deactivating the hook when eglot is not running resolves about 80% of the frustration for me. But I would like to fully resolve the issue. I don't actually want the format-on-save behavior in the first place. It has to be set somewhere. In the next section, I will briefly sketch out my process for identifying the issue.
Finding the root of the problem
My first thought was that, perhaps, the hook was being set when eglot
was invoked. To check this, I:
- Called
C-h f eglot
to find the documentation for theeglot
command - Followed the link in the help buffer to
eglot.el
, the source file where theeglot
command and related functions are defined. - Used
consult-line
(or, equivalently,isearch
or one of the many other tools available for searching buffer text) for the termhook
.
This showed me that eglot-format
was not, in fact, being set as an after-save-hook
function by eglot itself.
So…did I do this myself, somewhere in my config?
I next navigated to my config folder, ~/coffeemacs/
, and invoked lgrep
to search my various *.el
config files for anything related to eglot
.
And it turns out, I set this hook myself!

Once I deleted the (add-hook 'after-save-hook ...)
call from my config, the issue was fully resolved.
Conclusion—Emacs introspection
The approaches I used here are nowhere close to comprehensive. Emacs has countless introspection tools and a seemingly-inexhaustible collection of functions and variables that enable you to inspect everything going on in your Emacs setup. Furthermore, it provides a range of ways to search these variables and functions.
The following tools will go a long way toward helping you debug an error in Emacs:
- Enable debugging on error with
M-x toggle-debug-on-error
. This will provide a backtrace that will show the source of the error. - Search for relevant functions and variables with
apropos-function
andapropos-variable
. You can pass in a list of relevant terms to search for. - Get documentation for specific functions and variables with the
describe-function
(C-h f
) anddescribe-variable
(C-h v
) commands.
Even these relatively simple tools are often enough to identify the source of an issue and do something about it.
Lastly—we're reaching a point where you don't have to do this yourself. You can configure the gptel package with a set of tools—Emacs functions—that will enable it to recursively search for information in docs, manuals, source code, etc. This video provides a good overview of how to get started.
