Debugging Guide
April 13, 2026 · View on GitHub
Strategies for debugging Abies applications.
Overview
Debugging MVU applications follows a predictable pattern:
- Identify the symptom — What's wrong?
- Find the message — What triggered the issue?
- Trace the Transition — How did the model change?
- Check the View — Is the DOM correct?
- Verify Commands — Did side effects execute?
Abies Time Travel Debugger (Recommended)
For browser runtime apps, the Abies Time Travel Debugger is the primary debugging tool. In Debug builds, it auto-mounts when your app starts through Picea.Abies.Browser.Runtime.Run(...) and provides a complete trace of your MVU loop.
See devtools.md for the full guide, including:
- How to use the timeline to inspect every message and state transition
- Time travel: rewind your app to any past state
- How to disable the debugger if needed
- Troubleshooting tips
Abies includes built-in OpenTelemetry tracing that shows the complete flow from user interaction to API response.
Quick Start
- Open your app with Aspire AppHost running
- Go to the Aspire dashboard (Traces tab)
- Click through your app — traces appear automatically
- Click a trace to see the full waterfall
Verbosity Levels
| Level | What's Traced | How to Enable |
|---|---|---|
user | UI Events + HTTP (default) | Production default |
debug | Everything (DOM updates, etc.) | ?otel_verbosity=debug in URL |
off | Nothing | <meta name="otel-verbosity" content="off"> |
Runtime Toggle
Open browser console:
window.__otel.setVerbosity('debug');
window.__otel.getVerbosity();
await window.__otel.provider.forceFlush();
For the complete tracing tutorial, see Tutorial: Distributed Tracing.
Console Logging
Add temporary logging to trace message flow:
public static (Model, Command) Transition(Model model, Message msg)
{
Console.WriteLine($"[Transition] Message: {msg.GetType().Name}");
Console.WriteLine($"[Transition] Model before: {model}");
var (newModel, command) = msg switch
{
CounterMessage.Increment => (model with { Count = model.Count + 1 }, Commands.None),
CounterMessage.Decrement => (model with { Count = model.Count - 1 }, Commands.None),
_ => (model, Commands.None)
};
Console.WriteLine($"[Transition] Model after: {newModel}");
Console.WriteLine($"[Transition] Command: {command.GetType().Name}");
return (newModel, command);
}
In WASM, logs appear in the browser console (F12). On the server, logs go to stdout.
Debugging by Platform
Browser (WASM)
- Console (F12) —
Console.WriteLineoutput - Network — API calls and WebSocket traffic
- Elements — Inspect
data-event-*attributes for event handlers - Sources — Set breakpoints in C# files (Blazor debugging)
Server (InteractiveServer)
- Server logs —
Console.WriteLinegoes to server stdout - Browser DevTools — WebSocket tab shows DOM patches sent to client
- Aspire dashboard — Full distributed traces
- Debugger — Attach to the Kestrel process normally
Abies Time Travel Debugger
In browser runtime Debug sessions, Abies includes a built-in time-travel debugger that captures every message and model snapshot. You can step forward and backward through your app's history, replay sequences, and inspect model state at any point in time. The browser runtime injects the debugger mount point automatically when the debugger is enabled, and Release builds compile the debugger out entirely.
To disable it in Debug builds, configure it before calling Runtime.Run():
using Picea.Abies.Debugger;
DebuggerConfiguration.ConfigureDebugger(new DebuggerOptions { Enabled = false });
await Picea.Abies.Browser.Runtime.Run<MyProgram, MyModel, Unit>();
For full setup, keyboard shortcuts, the intent transport contract, and Release build details, see Abies DevTools: Time Travel Debugger.
Hot Reload Workflow (Debug)
Use this workflow when iterating on view functions.
No manual setup is required. Abies injects the hot reload metadata handler registration automatically in Debug builds.
1. Start the correct host with dotnet watch
Server host:
dotnet watch run --project MyApp.Server
WASM host (split structure):
dotnet watch run --project MyApp.Wasm.Host
WASM host (single-project structure):
dotnet watch run --project MyApp.Wasm
2. Edit view code and save
- Change
Viewor view helper functions. - Save the file and wait for hot reload to apply.
3. Verify state and UI
- Expected: Abies preserves current model state and re-renders the UI.
- Example: if a counter is at
42, it should stay at42after a pure view change.
Supported Modes
- Server host sessions (
*.Server, interactive modes) - Browser host runtime (
*.Wasm.Hostor*.Wasm)
Known Limitations
- This workflow is for view-function edits.
- Changes to
Initialize,Transition, command/interpreter code, or subscriptions require restart. - Some .NET edits are not hot-reloadable (rude edits); when that happens, restart the process.
Release Impact
Release builds are unaffected. Hot reload guidance in this section is Debug workflow only.
Debugging Transition
Print Model State
Add a debug panel to your view:
static Node DebugPanel(Model model) =>
details([], [
summary([], [text("Debug Info")]),
pre([], [text(System.Text.Json.JsonSerializer.Serialize(model,
new JsonSerializerOptions { WriteIndented = true }))])
]);
public static Document View(Model model) =>
new("App", div([], [
MainContent(model),
#if DEBUG
DebugPanel(model)
#endif
]));
Trace Message History
public record Model(
int Count,
List<string> MessageLog // Debug only
);
public static (Model, Command) Transition(Model model, Message msg)
{
var logEntry = $"{DateTime.Now:HH:mm:ss.fff} - {msg.GetType().Name}";
var newLog = model.MessageLog.Append(logEntry).TakeLast(20).ToList();
var (newModel, command) = msg switch { /* ... */ };
return (newModel with { MessageLog = newLog }, command);
}
Debugging Commands and Interpreters
Interpreter<Command, Message> interpreter = async command =>
{
Console.WriteLine($"[Interpreter] Executing: {command.GetType().Name}");
try
{
switch (command)
{
case LoadArticles:
var articles = await api.GetArticles();
Console.WriteLine($"[Interpreter] Loaded {articles.Length} articles");
return Result<Message[], PipelineError>.Ok([new ArticlesLoaded(articles)]);
default:
Console.WriteLine($"[Interpreter] Unhandled command: {command.GetType().Name}");
return Result<Message[], PipelineError>.Ok([]);
}
}
catch (Exception ex)
{
Console.WriteLine($"[Interpreter] Error: {ex.Message}");
return Result<Message[], PipelineError>.Ok([new CommandFailed(ex.Message)]);
}
};
Common Issues
Event Handler Not Firing
Symptoms: Clicking a button does nothing.
Check:
- Inspect the element — does it have a
data-event-clickattribute? - Add
Console.WriteLinein Transition — is the message arriving? - Ensure the handler is correctly attached:
onclick(new MyMessage())
Model Not Updating
Symptoms: UI doesn't reflect expected state.
// ❌ Mutation (records are immutable, this is a no-op)
model.Count = model.Count + 1;
return (model, Commands.None);
// ✅ Create new record with `with`
return (model with { Count = model.Count + 1 }, Commands.None);
View Not Reflecting Model
Symptoms: Model is correct but UI is stale.
- Log the model in View:
Console.WriteLine($"View: {model}") - Check if View is pure (no
DateTime.Now, no external state) - For lists, verify that keys/IDs are stable
API Calls Failing Silently
Symptoms: Data never loads, no errors shown.
- Check Network tab for the request
- Add try/catch with logging in the interpreter
- Check for CORS errors in the console
- Ensure the interpreter returns feedback messages
Navigation Not Working
Symptoms: URL changes but page doesn't update.
- Verify
Navigation.UrlChanges(...)is inSubscriptions - Log in the
UrlChangedmessage handler - Check route matching logic
Removing Debug Code
#if DEBUG
Console.WriteLine($"[Transition] {msg.GetType().Name}");
#endif
See Also
- MVU Architecture — Understanding message flow
- Testing — Catch bugs before they reach production
- Tutorial: Distributed Tracing — Full tracing setup