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
cabal replin your project.:break MyModule.suspectFunction— set a breakpoint.- Call the function with test inputs:
myFunction testInput. - Execution stops at the breakpoint. Use
:print,:force,:show bindings. :stepto move forward one reduction;:continueto run to the next breakpoint.:deletebreakpoints when done;:quitto 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
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 if not already installed. Then add a launch configuration to .vscode/launch.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. Usecabal replfor library/executables, orcabal repl my-package:test:my-testto 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 atSpec.hs.stopOnEntry— settrueto pause immediately at the first line;falseto 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:
- Click the left gutter in any
.hsfile to set a breakpoint (red dot). - Press F5 (or Run → Start Debugging) to launch.
- 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:
{
"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
logFileoutput. The most common cause ishaskell-debug-adapternot on PATH — confirm withwhich haskell-debug-adapter, and check thatcabal path --installdiris onPATHin the shell that launches the editor. - Breakpoint not hit: the module may be compiled rather than interpreted. Check that
cabal replis 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:forcefrom the debug console, or add!to the field in your data type. - DAP hangs on startup:
ghci-dapandhaskell-debug-adapterversions 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 — seehaskell-benchmarking. - Concurrency bugs: the debugger doesn't handle concurrent execution well.
Debug.Trace.traceEventIOwith 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
runMyEffectcall in the interpreter stack.
Related
- GHCi
:traceand space leaks:haskell-benchmarkingfor profiling and flame graphs. - Testing strategies that reduce the need for debugging:
haskell-testing.