Daniel Liden

Blog / About Me / Photos / Notebooks / Notes /

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:

  1. 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.
  2. 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!

1_apropos.png

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:

  1. The value of before-save-hook is nil.
  2. 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:

  1. Called C-h f eglot to find the documentation for the eglot command
  2. Followed the link in the help buffer to eglot.el, the source file where the eglot command and related functions are defined.
  3. Used consult-line (or, equivalently, isearch or one of the many other tools available for searching buffer text) for the term hook.

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!

2_grep.png

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:

  1. Enable debugging on error with M-x toggle-debug-on-error. This will provide a backtrace that will show the source of the error.
  2. Search for relevant functions and variables with apropos-function and apropos-variable. You can pass in a list of relevant terms to search for.
  3. Get documentation for specific functions and variables with the describe-function (C-h f) and describe-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.

3_gptel.png

Date: 2025-03-23 Sun 00:00

Emacs 29.3 (Org mode 9.6.15)