DEV Community

Marie K. Ekeberg
Marie K. Ekeberg

Posted on • Originally published at themkat.net

Emacs Lisp debugging tips

Have you ever gotten weird errors in an Emacs Lisp package? Something like "wrong type argument" or similar shown in the minibuffer? At first glance, these seem kind of cryptic. Where do they come from? Can I get a stack trace? What arguments are functions called with? Today I will show you how to answer these questions!

Show stack traces

You have probably gotten one of those weird "wrong type argument" errors, or similar errors just printed in the minibuffer before. You have probably also navigated to the *Messages*-buffer to check if there is any additional information. Did you know you can see a stack trace instead? Then you can see the functions being called leading up to the error!

There are several ways to activate it. The most common way is to just set Emacs to enter the debugger when an error happen. You can do this by setting the variable debug-on-error
to true (either in your config or just executing it right in your scratch buffer):

  (setq debug-on-error t)
Enter fullscreen mode Exit fullscreen mode

I usually prefer to turn it on by need using M-x instead, M-x toggle-debug-on-error.

Let's show a quick example using a function from the lsp-mode codebase. Experienced programmers will probably notice the issue right away, but let's see how we will debug it. We are going to assume that we have gotten a hash table that we want to fetch results from called result-data. Running it in the scratch buffer will always show a stack trace, so let's make an interactive function we will call using M-x:

  (defun my-lsp-mode-fun ()
    (interactive)
    (let* ((my-key "key")
           (my-data (lsp-get result-data my-key)))
      ;; ... do something with the data ...
      ))
Enter fullscreen mode Exit fullscreen mode

If we call it without turning on debug-on-error, we will see the error Wrong type argument: symbolp, "key" in the minibuffer and messages-buffer. Some of you will probably already see that it expects a symbol, not a string. It will be even more clear if you turn on debug-on-error and running it again:

  Debugger entered--Lisp error: (wrong-type-argument symbolp "key")
    symbol-name("key")
    lsp-keyword->string("key")
    lsp-get(#<hash-table equal 0/65 0x47fa6ca9> "key")
    (let* ((my-key "key") (my-data (lsp-get result-data my-key))))
    my-lsp-mode-fun()
    funcall-interactively(my-lsp-mode-fun)
    call-interactively(my-lsp-mode-fun record nil)
    command-execute(my-lsp-mode-fun record)
    helm-M-x-execute-command(my-lsp-mode-fun)
    helm-execute-selection-action-1()
    helm-execute-selection-action()
    helm-internal((((name . "Emacs Commands history") ...truncated....
    helm-M-x(nil)
    funcall-interactively(helm-M-x nil)
    call-interactively(helm-M-x nil nil)
    command-execute(helm-M-x)

Enter fullscreen mode Exit fullscreen mode

(some data are truncated for better readability by me)

You see that we get a stack trace of all operations that is done by Emacs. Even if you are not making packages yourself, this can prove very useful to provide information in issue trackers when reporting issues (e.g, on the packages Github repo).

There are several other ways to toggle errors, ranging from message patterns to signals. You can read more about that on the documentation pages.

Tracing function invocations

Another useful troubleshooting tips to know is tracing function invocations/calls. Sometimes we just want to see what arguments a specific function is called with, and be notified about it each time it is run. This is done with the =trace-function= command (M-x trace-function, followed by the name of the function you want to trace).

Let's reuse the code example from above and trace lsp-get. After calling our interactive function once, we will then see a buffer called *trace-output* with data like this:

  ======================================================================
  1 -> (lsp-get #s(hash-table size 65 test equal rehash-size 1.5 rehash-threshold 0.8125 data ()) "key")
  1 <- lsp-get: !non-local\ exit!

Enter fullscreen mode Exit fullscreen mode

Calling it more times will populate the buffer with more data. You can trace as many functions as you want. When you are done tracing a function, you can call untrace-function and choose the function you want to stop tracing.

I find this super useful when developing packages. When calling a function external to your package (i.e, from another package or built-in to Emacs Lisp), it's very useful to be able to trace how functions behave. This includes when, or if, they are called, as well as with what arguments.

Top comments (0)