> ## Content Index
> Fetch the complete content index at: https://til.codes/llms.txt
> Use this file to discover other available public pages before exploring further.

# Beyond IO.inspect: The Holy Trinity of Elixir & Phoenix Debugging with Neovim and Nix
- URL: https://til.codes/beyond-io-inspect-the-holy-trinity-of-elixir-phoenix-debugging-with-neovim-and-nix/
- Published: 2025-07-25T02:57:22.000Z
- Updated: 2025-07-25T02:57:22.000Z
- Description: A guide on how to set up the Elixir debugger in Neovim. This post explores the complete configuration using elixir-ls and nvim-dap, with a special focus on making it work seamlessly inside a Nix development environment.
- Author: Manu Ajith
- Tags: neovim, nix, elixir

Setting up a proper Elixir debugger in Neovim can feel like a dark art, especially when you throw Nix and `devenv` into the mix. I recently went down this rabbit hole, trying to get `nvim-dap` to play nicely with ElixirLS for a Phoenix project, and let me tell you, it was a journey.

It was filled with cryptic errors, silent failures, and some moments of pure frustration. 

## The First Hurdle: Talking to the Wrong Adapter

Right out of the gate, my debugger failed to launch. My initial `nvim-dap` config was simple, but it was pointing to the generic `elixir-ls` command. It turns out ElixirLS ships with two different scripts:

- `language_server.sh` for all that sweet LSP goodness.
- `debug_adapter.sh` for... well, debugging.

My setup was calling the wrong number.

**The Fix:** I had to explicitly point DAP to the `debug_adapter.sh` script provided by Mason. A simple path change, and one problem down.

```lua
dap.adapters.mix_task = {
  type = 'executable',
  -- Point directly to the debug adapter script!
  command = vim.fn.expand('~/.local/share/nvim/mason/packages/elixir-ls/debug_adapter.sh'),
  args = {}
}

```

## The Nix Saga: ElixirLS in a Cage

My victory was short-lived. Since my Elixir installation is managed entirely by Nix/devenv (meaning it's not on my system's global `PATH`), the Mason-installed ElixirLS threw a fit. It couldn't find the `elixir` executable and crashed.

```
** (FunctionClauseError) no function clause matching in IO.chardata_to_string/1

```

I needed a way to make the configuration smart enough to use the Nix-provided environment when available.

**The Solution:** Lua to rescue. I wrote a helper function that first checks if `elixir-ls` is in the `PATH` (which it will be inside a `devenv shell`). If found, it uses the `debug_adapter.sh` from that Nix-provided location. If not, it gracefully falls back to the default Mason path.

```lua
local function get_elixir_ls_debug_adapter()
  -- Check if elixir-ls is in the shell's PATH (from Nix/devenv)
  local elixir_ls = vim.fn.exepath('elixir-ls')
  if elixir_ls ~= '' then
    local dir = vim.fn.fnamemodify(elixir_ls, ':h')
    local debug_adapter = dir .. '/debug_adapter.sh'
    if vim.fn.filereadable(debug_adapter) == 1 then
      vim.notify("Found Nix environment, using adapter: " .. debug_adapter)
      return debug_adapter
    end
  end
  -- Otherwise, fall back to the Mason-installed one
  local mason_adapter = vim.fn.expand("~/.local/share/nvim/mason/packages/elixir-ls/debug_adapter.sh")
  vim.notify("Using Mason debug adapter: " .. mason_adapter)
  return mason_adapter
end

```

## The Silent Server: Getting Phoenix to Cooperate

With the adapter path sorted, I tried to launch a debug session for my Phoenix server... and nothing. The debugger would start, print a "Sleeping..." message, and then just sit there, mocking me. The server never actually booted up. 

After some time with the ElixirLS docs, I discovered that debugging Phoenix apps has some special rules.

**The Fix:** The launch configuration needed a few key tweaks. You can't just `mix phx.server` and hope for the best.

- You must use `debugInterpretModulesPatterns` to tell the debugger **only to interpret your own app's modules**. Otherwise, it tries to load everything and chokes.
- Phoenix's live reload is **incompatible** with the debugger.
- Don't set `startApps = true`.

```lua
dap.configurations.elixir = {
  {
    type = "mix_task",
    name = "phoenix server",
    task = "phx.server",
    request = "launch",
    
    projectDir = "${workspaceFolder}",
    -- Tell the debugger which modules are yours!
    debugInterpretModulesPatterns = {"MyCoolApp*", "MyCoolAppWeb*"},
    -- This is important for Phoenix
    exitAfterTaskReturns = false,
  },
}

```

## The Mute REPL & Annoying Errors

I was getting closer. I could launch the debugger and hit a breakpoint, but two smaller issues remained:

1. My DAP REPL was useless, screaming `No active session` whenever I tried to inspect a variable.
2. A constant warning about `unsupported exception breakpoints` was cluttering my screen.

**The Fixes:** The first one was a classic user error. The REPL only works when execution is actually **paused at a breakpoint**. For the second, it's just an expected behavior, Elixir's debugger doesn't support exception breakpoints. A single line of config silenced it for good.

```lua
dap.defaults.elixir.exception_breakpoints = {}

```

## Complete, Working Config

After all that troubleshooting, here is the complete, battle-tested configuration for your `nvim-dap` setup. It includes the adapter logic, correct Phoenix settings, a test runner, and some nice UI/keymap defaults.

```lua
return {
  {
    "mfussenegger/nvim-dap",
    dependencies = {
      "rcarriga/nvim-dap-ui",
      "nvim-neotest/nvim-nio",
      "williamboman/mason.nvim",
    },
    config = function()
      local dap = require "dap"
      local dapui = require "dapui"

      -- A nice, spacious UI layout
      dapui.setup({
        layouts = {
          {
            elements = { { id = "scopes", size = 0.25 }, "breakpoints", "stacks", "watches" },
            size = 40,
            position = "left",
          },
          {
            elements = { "repl", "console" },
            size = 0.25,
            position = "bottom",
          },
        },
      })

      -- Smart adapter detection for Nix/devenv vs. Mason
      local function get_elixir_ls_debug_adapter()
        local elixir_ls = vim.fn.exepath('elixir-ls')
        if elixir_ls ~= '' then
          local dir = vim.fn.fnamemodify(elixir_ls, ':h')
          local debug_adapter = dir .. '/debug_adapter.sh'
          if vim.fn.filereadable(debug_adapter) == 1 then
            vim.notify("Found Nix environment, using adapter: " .. debug_adapter, vim.log.levels.INFO)
            return debug_adapter
          end
        end
        local mason_adapter = vim.fn.expand "~/.local/share/nvim/mason/packages/elixir-ls/debug_adapter.sh"
        vim.notify("Using Mason debug adapter: " .. mason_adapter, vim.log.levels.INFO)
        return mason_adapter
      end

      dap.adapters.mix_task = {
        type = "executable",
        command = get_elixir_ls_debug_adapter(),
        args = {},
      }

      -- Silence the unsupported exception breakpoint warning
      dap.defaults.elixir.exception_breakpoints = {}

      dap.configurations.elixir = {
        -- Phoenix server config
        {
          type = "mix_task",
          name = "phoenix server",
          task = "phx.server",
          request = "launch",
          projectDir = "${workspaceFolder}",
          exitAfterTaskReturns = false,
          debugAutoInterpretAllModules = false,
          -- IMPORTANT: Change these patterns to match your app!
          debugInterpretModulesPatterns = {"MyCoolApp*", "MyCoolAppWeb*"},
          env = { MIX_ENV = "dev" },
        },
        -- Mix test config
        {
          type = "mix_task",
          name = "mix test",
          task = "test",
          taskArgs = {"--trace"},
          request = "launch",
          projectDir = "${workspaceFolder}",
          requireFiles = {
            "test/**/test_helper.exs",
            "test/**/*_test.exs"
          },
        },
      }

      -- Auto open/close DAP UI
      dap.listeners.after.event_initialized["dapui_config"] = function() dapui.open() end
      dap.listeners.before.event_terminated["dapui_config"] = function() dapui.close() end
      dap.listeners.before.event_exited["dapui_config"] = function() dapui.close() end

      -- Handy Keymaps
      vim.keymap.set("n", "<Leader>db", dap.toggle_breakpoint, { desc = "Toggle Breakpoint" })
      vim.keymap.set("n", "<F5>", dap.continue, { desc = "Continue (F5)" })
      vim.keymap.set("n", "<F10>", dap.step_over, { desc = "Step Over (F10)" })
      vim.keymap.set("n", "<F11>", dap.step_into, { desc = "Step Into (F11)" })
      vim.keymap.set("n", "<F12>", dap.step_out, { desc = "Step Out (F12)" })

    end,
  },
}

```

## Final Tips & Gotchas

- **Launch from Nix!** Always, always, *always* start Neovim from within your `devenv shell`. This is non-negotiable.
- **Set Your Modules:** Remember to change `debugInterpretModulesPatterns` to match your application's module names (e.g., `MyApp*`, `MyAppWeb*`).
- **REPL Usage:** The REPL only works when you're paused at a breakpoint.
- **No Live Reload:** Phoenix's live reload feature is disabled during a debug session. You'll have to manually restart if you make changes.

Getting Neovim, DAP, ElixirLS, and Nix to work together was a challenge, but the payoff is a powerful, integrated debugging experience right in your editor. No more `IO.inspect/1` spam

Happy debugging!