Diagnosing State Bugs with Elm's Time Traveling Debugger: A Practical Guide
A diagnostic guide for using Elm's Time Traveling Debugger to trace state bugs: recognizable symptoms, cause table, ordered checks, fixes tied to findings, and escalation criteria.
15 Sept 2026, 13:32 UTC

When State Changes Don't Match Expectations
You've added a feature, tests pass locally, but users report intermittent UI glitches—counters desync, form fields reset, or navigation state gets stuck. The bug doesn't reproduce in a clean session. Elm's Time Traveling Debugger lets you replay the exact sequence of states and actions that led to the failure, turning "works on my machine" into a reproducible trace.
Recognizable Condition: State Divergence Without Clear Cause
The debugger shines when:
- Application state appears correct after each user action but drifts over time
- Multiple components share state through a parent and updates race
- Ports or subscriptions introduce external events that bypass normal update flow
- Hot reloading during development loses context of prior state transitions
Cause and Diagnostic Quick-Reference
| Symptom | Likely Cause | Debugger Check |
|---|---|---|
| State reverts unexpectedly | Parent reinitializes child on unrelated update | Inspect init calls in history; look for duplicate Model construction |
| Action logged but state unchanged | Update returns (model, Cmd.none) for unhandled message | Filter history by message type; verify model diff is empty |
| State jumps between two values | Competing subscriptions firing same message | Check timestamp ordering; correlate with Sub.batch sources |
| Debugger panel empty or frozen | Production build or --optimize flag strips instrumentation | Confirm elm make --debug flag; check bundle for elm-debugger module |
Ordered Diagnostic Checks
1. Verify Debug Build Configuration
Run from project root with Elm 0.19.1+:
elm make src/Main.elm --debug --output=debug.js
Required: Write permission to output directory. Placeholder: src/Main.elm is your entry point. Check: Output contains _Debugger namespace. Risk: Debug bundle is 2-3× larger; never ship to production.
2. Open Debugger and Reproduce
Load the generated debug.js in a browser. Open DevTools → Console, then click the Elm debugger badge (bottom-right). Perform the failing user flow. The timeline populates with each Msg and resulting Model.
3. Filter to Relevant Messages
Use the message type filter (top of panel) to isolate the domain—e.g., type UserInput to hide Tick or WebSocket noise. Click any row to inspect the full model at that step.
4. Compare Consecutive States
Select two adjacent rows. The diff view highlights added/removed/changed fields. For nested records, expand triangles to drill into child component state. Look for:
- Fields reset to default values (suggests reinitialization)
- Arrays growing unexpectedly (duplicate subscriptions)
Maybeflipping betweenJustandNothingwithout clear trigger
5. Export Trace for Offline Analysis
Click "Export" in the debugger UI. Save the JSON file. It contains the full action log and state snapshots. Share with teammates or load in a separate session via "Import" to reproduce without re-running the app.
Fixes Tied to Findings
Problem: Child Component Reinitialized on Parent Update
Evidence: Diff shows child's entire model replaced with initialModel after parent handles unrelated message.
Fix: Ensure parent's view passes stable key or uses Html.keyed for lists. For single child, avoid reconstructing child model in parent's update—delegate to child's own update via Child.update msg childModel.
Problem: Unhandled Message Silently Dropped
Evidence: Message appears in timeline but model diff is empty; no Cmd emitted.
Fix: Add exhaustive pattern match in update. Elm 0.19 warns on missing cases, but _ -> (model, Cmd.none) catch-all hides bugs. Replace with explicit cases or use Debug.todo for truly impossible messages.
Problem: Duplicate Subscription Registrations
Evidence: Same Msg fires twice per external event; timeline shows paired entries with identical timestamps.
Fix: Move subscription to top-level subscriptions function. Avoid creating subscriptions inside view or component init. Use Sub.batch once with all sources.
Limitations and Practical Verification
- Performance: Apps with >500 state snapshots may lag. Clear history periodically via debugger UI or limit session length.
- Sensitive Data: Auth tokens, PII appear in exported traces. Sanitize before sharing; consider a custom
toJsonthat redacts fields. - No Production Access: Debugger only works in
--debugbuilds. For production issues, reproduce locally with same steps or add structured logging toportendpoints. - Verification: After a fix, re-run the failing flow with debugger open. Confirm the problematic diff no longer appears. Export trace and diff against pre-fix export using
jqor a JSON diff tool.
Escalation Criteria
Escalate beyond debugger when:
- Bug requires specific timing (race conditions with ports/WebSockets) that debugger's single-threaded replay masks
- State size exceeds browser memory (exported JSON >50 MB)
- Issue only appears in optimized production build—add temporary
Debug.logstatements and rebuild with--optimizefor targeted inspection - Multiple developers need simultaneous access—set up a shared staging environment with debug build deployed
Quick Verification Checklist
- Build with
elm make --debugand confirm_Debuggerin output - Reproduce issue while timeline records
- Filter to suspect message types
- Diff adjacent states at failure point
- Apply fix, rebuild, re-verify timeline shows expected transitions
- Export clean trace for regression test archive
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.