Common config anti-patterns

Over the years, I’ve noticed common issues in the private configs of Doom’s users. Here, I document those that stand out most (and are caused by misunderstanding rather than bugs), with hopeful solutions. Many of these cause subtle issues that can lead to misbehavior down the road.


add-hook! with an explicit lambda

The problem: you may be surprised that this hook never runs:

(add-hook! 'python-mode-hook
  (lambda ()
    (setq python-indent-offset 2)))

Be careful with the differences between add-hook and Doom’s add-hook! convenience macro. add-hook! will implicitly wrap list arguments in (lambda (&rest) ...). What the above actually ends up doing is:

(add-hook 'python-mode-hook
  (lambda (&rest _)
    (lambda ()
      (setq python-indent-offset 2))))

The solution: Either use add-hook (which accept the lambda the way you were expecting), or remove the lambda and use add-hook!:

(add-hook 'python-mode-hook
  (lambda ()
    (setq python-indent-offset 2)))
;; or
(add-hook! 'python-mode-hook
  (setq python-indent-offset 2))

Passing after! a major or minor mode

The problem: the first argument of the after! macro is not a major or minor mode, but the name of a package. For example:

;; Do
(after! python ...)
(after! flycheck ...)
;; Don't
(after! python-mode ...)
(after! flycheck-mode ...)

The latter is a common mistake; it is never evaluated, and users will wonder why their code isn’t having an effect.

The solution: Use the package’s name, instead. What package to use can be determined using Doom’s documentation facilities:

  • SPChf python-mode
  • C-hf python-mode (for users with evil disabled)

These display the documentation for python-mode in a popup. The first line of the documentation will indicate the package that defines it:

python-mode is an autoloaded and interactive function defined in python.el.gz.

python.el.gz is the package’s file name. Strip away the extensions and you have the package’s name (i.e. python).

:warning: To add to the confusion: some packages are named after a mode they define. e.g. The js2-mode package defines js2-mode. In this case, (after! js2-mode ...) would be correct.

Loading packages too early

The problem: Doom lazy loads (almost) all of its packages, i.e. they are not loaded immediately, but when you need them. A common mistake is to misuse the use-package (or use-package!) macros, causing packages to be eagerly loaded.

(use-package! X
  :config
  ..)

The expectation is that ... is evaluated later, when X is loaded, but what actually happens is X is immediately loaded at startup (then ... is evaluated).

The solution:

(use-package! X
  :defer t
  :config
  ...)

;; or

(after! X
  ...)

:pushpin: use-package has other properties that imply :defer t. e.g. :hook, :mode, :commands, and :after.

Using exec-path-from-shell

The problem: Your PATH and other environment variables are sometimes unavailable in GUI Emacs (this is especially true for macOS users). The exec-path-from-shell package was written to correct this, by launching your shell and scraping its environment when you start up Emacs.

So what’s the problem? Doom comes with its own, better version of this. If you weren’t aware, you’ll likely benefit from switching to it. I go into further detail elsewhere.

The solution: Run doom env in the command line instead. This scrapes your current shell environment into an envvar file which Doom loads at startup.

You only need to do this once. Your envvar file is regenerated each time you doom sync thereafter.

Installing packages with package.el

The problem: Doom has its own package manager (powered by straight.el). It is far more powerful than the package manager built into Emacs (called package.el).

However, most package documentation will tell you to install packages with M-x package-install or with an :ensure t in a use-package block. Doom won’t stop you from doing so if you know what you’re doing, but in most cases, the user simply isn’t aware of Doom’s package manager.

The fix: Use Doom’s package manager instead. This section in the manual about package management goes into the details, but the highlights are:

  • To install a package: add (package! package-name) to ~/.doom.d/packages.el, then execute $ doom sync on the command line.
  • Do not use :ensure t in your (use-package ...) declarations, as it uses package.el under the hood
  • Run $ doom sync -u if you’ve changed the recipe of an existing package and want to update your packages.

Loading Org babel plugins yourself

The problem: You might have needed something like this in your previous config:

(org-babel-do-load-languages
 'org-babel-load-languages
 '((R . t)
   (python . t)))

This practice is recommended in the documentation of by many Org babel plugins, but this is unnecessary in Doom Emacs. Babel has been modified to lazy-load your babel plugins. No additional configuration is needed on your part unless the package has special load requirements (such as jupyter – which Doom will set up for you with :lang org’s +jupyter flag).

The solution: remove calls to org-babel-do-load-languages in your config.

Calling server-start yourself

The problem: Users may be calling (server-start) themselves, in their config. This isn’t necessary, because Doom starts the server for you, after a brief delay, after your first input, or when you first unfocus Emacs.

The solution: Don’t start it yourself.

18 Likes