# Haskell Debugging

> Debug Haskell programs interactively using the GHCi debugger and DAP (Debug Adapter Protocol) adapters. Covers the three DAP packages (haskell-dap, ghci-dap, haskell-debug-adapter), VS Code/editor launch.json configuration, GHCi breakpoints and stepping, and when to reach for each tool. Use when setting up debugger integration, stepping through runtime behavior, or diagnosing issues that tracing alone can't reveal.

- Skill: `ivelten/haskell-debugging` (Agent Skill)
- Install (CLI): `npx skillmds@latest add ivelten/haskell-debugging`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ivelten/haskell-debugging/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: ivelten (https://skillmd.com/u/ivelten)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/ivelten/haskell-debugging

---


# Haskell Debugging

Two complementary approaches: the **GHCi built-in debugger** (fast, terminal-based, no setup beyond GHCi) and **DAP adapters** (integrated into VS Code and other editors with a GUI).

## GHCi debugger

The GHCi debugger is always available — no extra packages. Load your module with `cabal repl` or `ghci`, then use these commands:

### Setting breakpoints

```
-- By function name
:break MyModule.myFunction

-- By module and line number
:break MyModule 42

-- List active breakpoints
:show breaks

-- Delete a breakpoint by index (from :show breaks output)
:delete 0
```

### Stepping through execution

```
-- Run until next breakpoint; starts evaluation
:continue

-- Step into the next redex (one reduction)
:step

-- Step over (run to next line without entering calls)
:steplocal

-- Step to next breakpoint in a specific module
:stepmodule MyModule
```

### Inspecting state at a breakpoint

```
-- Print a variable (forces evaluation)
:print x

-- Force a thunk and print (like :print but evaluates completely)
:force x

-- Show current locals in scope
:show bindings

-- Evaluate an expression in the current context
x + 1
```

### Tracing (non-interactive)

When you don't want to step but want a call history:

```
-- Run with tracing enabled; builds a call history
:trace myFunction arg1

-- Show the call history after a breakpoint or exception
:history

-- Show the back trace
:back
```

### Catching exceptions

```
-- Break on any exception
:set -fbreak-on-exception

-- Break only on errors (not pure exceptions)
:set -fbreak-on-error

-- Disable
:unset -fbreak-on-exception
```

### Practical GHCi debugging workflow

1. `cabal repl` in your project.
2. `:break MyModule.suspectFunction` — set a breakpoint.
3. Call the function with test inputs: `myFunction testInput`.
4. Execution stops at the breakpoint. Use `:print`, `:force`, `:show bindings`.
5. `:step` to move forward one reduction; `:continue` to run to the next breakpoint.
6. `:delete` breakpoints when done; `:quit` to exit.

The GHCi debugger operates on interpreted code, not compiled code. If a module is compiled (with `-O`), breakpoints won't fire in it. Use `cabal repl` which loads library modules interpreted.

---

## DAP (Debug Adapter Protocol)

DAP adapters expose the GHCi debugger as a protocol that editors understand — enabling GUI breakpoints, variable panels, and step buttons. Three packages work together:

| Package | Role |
|---|---|
| `haskell-dap` | Implements the DAP protocol layer |
| `ghci-dap` | Wraps GHCi with DAP-compatible I/O |
| `haskell-debug-adapter` | The adapter binary that editors talk to |

### Installation

```bash
cabal install haskell-dap ghci-dap haskell-debug-adapter
```

All three must be installed together — they depend on each other's types. They are built against the active GHC, so reinstall them after changing the pinned compiler. They land in cabal's install directory (`cabal path --installdir`), which must be on `PATH`.

### VS Code configuration

Install the [Haskell extension](https://marketplace.visualstudio.com/items?itemName=haskell.haskell) if not already installed. Then add a launch configuration to `.vscode/launch.json`:

```json
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "ghc",
      "request": "launch",
      "name": "Debug Haskell (cabal repl)",
      "ghci": "cabal repl",
      "startup": "test/Spec.hs",
      "stopOnEntry": false,
      "logFile": "/tmp/haskell-debug-adapter.log",
      "logLevel": "WARNING"
    }
  ]
}
```

Key fields:
- **`ghci`** — the command to launch GHCi. Use `cabal repl` for library/executables, or `cabal repl my-package:test:my-test` to load a specific test suite.
- **`startup`** — a Haskell file to load first. For a library, this is typically your entry module. For a test suite, point at `Spec.hs`.
- **`stopOnEntry`** — set `true` to pause immediately at the first line; `false` to run until the first breakpoint.
- **`logFile`** — path for adapter debug output. Useful when the debugger fails to start.

### Setting breakpoints in VS Code

Once the launch configuration is set:
1. Click the left gutter in any `.hs` file to set a breakpoint (red dot).
2. Press **F5** (or Run → Start Debugging) to launch.
3. VS Code shows locals, call stack, and a watch panel when execution pauses.

Breakpoints set in the editor translate to `:break` commands sent to `ghci-dap`.

### Debugging a specific test

To debug a failing hspec test rather than the full suite:

```json
{
  "type": "ghc",
  "request": "launch",
  "name": "Debug failing test",
  "ghci": "cabal repl my-project:test:my-project-test",
  "startup": "test/Spec.hs",
  "stopOnEntry": false
}
```

Set a breakpoint in the function the test exercises, then trigger the test from the GHCi panel: `hspec spec`.

### Troubleshooting DAP

- **Adapter doesn't start**: check `logFile` output. The most common cause is `haskell-debug-adapter` not on PATH — confirm with `which haskell-debug-adapter`, and check that `cabal path --installdir` is on `PATH` in the shell that launches the editor.
- **Breakpoint not hit**: the module may be compiled rather than interpreted. Check that `cabal repl` is loading the module as interpreted (no `*` prefix on the module name in the GHCi prompt means it's compiled).
- **Variables show `_thunk`**: the value hasn't been forced yet. Use `:force` from the debug console, or add `!` to the field in your data type.
- **DAP hangs on startup**: `ghci-dap` and `haskell-debug-adapter` versions must be compatible. Reinstall all three together to ensure version alignment.

---

## Choosing between GHCi and DAP

| Situation | Prefer |
|---|---|
| Quick ad-hoc investigation | GHCi debugger (`:break`, `:step` in terminal) |
| Persistent breakpoints during a debugging session | DAP (VS Code GUI, no retyping `:break`) |
| Debugging a specific test case | Either — DAP is more convenient for re-running |
| Remote or headless environment | GHCi debugger (no editor required) |
| Unfamiliar codebase — exploring call paths | `:trace` + `:history` in GHCi |

## When debugging isn't the right tool

- **Type-level bugs**: the compiler usually surfaces these at build time. The debugger operates at runtime on evaluated values, not types.
- **Performance issues**: reach for profiling (`-prof -fprof-auto`, `+RTS -s`, `ghc-prof-flamegraph`) rather than the debugger — see `haskell-benchmarking`.
- **Concurrency bugs**: the debugger doesn't handle concurrent execution well. `Debug.Trace.traceEventIO` with ThreadScope gives better visibility into concurrent behavior.
- **Effect handler wiring**: if effects aren't resolving correctly, add explicit type annotations and let the compiler guide you — runtime debugging can't surface a missing `runMyEffect` call in the interpreter stack.

## Related

- GHCi `:trace` and space leaks: `haskell-benchmarking` for profiling and flame graphs.
- Testing strategies that reduce the need for debugging: `haskell-testing`.

