Tracing
Tracing gives each fiber a running breadcrumb trail of the IO operations it has executed. When an error occurs, the breadcrumb trail is surfaced as the error's stack trace — giving you a high-level view of what the fiber was doing rather than a wall of internal Dart frames.
The problem tracing solves
Dart's native stack trace points at the internals of the IO runtime (the interpreter loop, trampolining, continuation stacks) rather than at your application code. When a deeply nested IO pipeline fails, the native trace is often unhelpful:
#0 _IOFiber._runLoop (fiber.dart:212)
#1 _IOFiber._step (fiber.dart:184)
#2 ...
With tracing enabled, the error carries an IOFiberTrace instead — a list of
labeled IO operations ordered from oldest to most recent:
IOFiberTrace:
flatMap @ package:myapp/service.dart:38:10
delay @ package:myapp/service.dart:35:8
Enabling tracing
Tracing is disabled by default to avoid any overhead in production. Enable
it once at application startup, before any IO runs:
void enableTracing() {
// Tracing is disabled by default. Enable it once at application startup,
// before any IO runs.
IOTracingConfig.tracingEnabled = true;
// The trace buffer is a ring buffer that keeps the last N operations per
// fiber. The default is 64. Raise it for deeper traces; lower it to
// reduce memory overhead on memory-constrained targets.
IOTracingConfig.traceBufferSize = 128;
}
traceBufferSize controls the depth of the ring buffer kept per fiber.
Entries are overwritten in FIFO order once the buffer is full, so only the
most recent N operations appear in the trace. The default of 64 is enough
for most debugging sessions.
Enable tracing in development and CI, and leave it disabled in production
builds. Because the check happens when IO values are constructed, disabling
it is truly zero-cost — no _Traced wrapper nodes are created at all.
Inspecting a trace on error
// When tracing is enabled, error outcomes carry an IOFiberTrace instead of
// a native Dart stack trace. The trace shows the labeled operations that
// ran before the failure, most recent first.
Future<void> tracingError() async {
IOTracingConfig.tracingEnabled = true;
final outcome =
await IO
.delay<int>(() => throw Exception('database unavailable'))
.flatMap((int n) => IO.pure(n * 2))
.unsafeRunFutureOutcome();
outcome.fold(
() => print('cancelled'),
(Object err, StackTrace? trace) {
print('Error: $err');
// When tracing is on, trace is an IOFiberTrace — a labeled breadcrumb
// trail of the IO operations that ran before the failure.
//
// Example output:
//
// IOFiberTrace:
// flatMap @ package:myapp/main.dart:12:10
// delay @ package:myapp/main.dart:11:8
if (trace is IOFiberTrace) {
print(trace);
}
},
(int result) => print('result: $result'),
);
}
The StackTrace? argument in the errored branch of Outcome.fold is an
IOFiberTrace when tracing is on. It implements StackTrace, so it works
with any existing error-reporting code that accepts a StackTrace. Casting
to IOFiberTrace is only needed if you want to access the raw List<String>.
Automatic labels
You do not need to call .traced() manually for everyday operations.
All built-in IO factory methods and combinators — delay, flatMap,
race, sleep, bracket, start, and around forty others — automatically
record a label when tracing is enabled. Your own code appears in traces
through the operations you compose.
Fiber dump
A fiber dump is a one-shot snapshot of every active fiber: its current status and the contents of its trace buffer. It is useful for diagnosing hangs, deadlocks, and unexpected concurrency behaviour — situations where there is no error outcome to inspect.
Manual dump
// Call IOFiber.dumpFibers() at any point to print a snapshot of every active
// fiber — its status and the operations in its current trace buffer.
//
// Example output:
//
// ===== FIBER DUMP (3 active) ===================
// Fiber #0 [RUNNING (or scheduled)]
// ├ at race @ package:myapp/main.dart:42:5
// ├ at flatMap @ package:myapp/main.dart:38:3
// ╰ at start @ package:myapp/main.dart:35:3
//
// Fiber #1 [SUSPENDED: Sleep]
// (No trace)
//
// Fiber #2 [SUSPENDED: Async(waiting for callback)]
// ╰ at async @ package:myapp/main.dart:29:7
//
// ================================================
IO<Unit> manualDump() {
return IO.delay<Unit>(() {
IOFiber.dumpFibers();
return Unit();
});
}
IOFiber.dumpFibers() prints to stdout immediately. The fiber status line
shows whether the fiber is running or what it is suspended on:
| Status | Meaning |
|---|---|
RUNNING (or scheduled) | Actively executing or queued to run |
SUSPENDED: Sleep | Waiting on IO.sleep |
SUSPENDED: Cede | Yielded via IO.cede |
SUSPENDED: Async(waiting for callback) | Blocked on an async callback |
Signal-triggered dump
On native Dart targets you can install OS signal handlers so a dump fires from a terminal without touching the running program:
// On native Dart targets, install OS-level signal handlers so a dump can be
// triggered from a terminal without modifying the running application:
//
// Ctrl+\ → sends SIGQUIT
// kill -SIGUSR1 <pid> → sends SIGUSR1
//
// Both signals print the fiber dump and resume execution immediately.
IO<Unit> setupSignalHandler() {
return IO.delay<Unit>(() {
IO.installFiberDumpSignalHandler();
return Unit();
});
}
| Signal | How to send |
|---|---|
SIGQUIT | Ctrl+\ in the terminal running the program |
SIGUSR1 | kill -SIGUSR1 <pid> from another shell |
Both signals print the dump and resume the application immediately — the program does not exit. Signal handlers are not available on web targets or Windows.
Call IO.installFiberDumpSignalHandler() near the top of main so it is
always available during development. It is a no-op if signals are
unsupported on the current platform.