Skip to main content

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.

tip

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:

StatusMeaning
RUNNING (or scheduled)Actively executing or queued to run
SUSPENDED: SleepWaiting on IO.sleep
SUSPENDED: CedeYielded 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();
});
}
SignalHow to send
SIGQUITCtrl+\ in the terminal running the program
SIGUSR1kill -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.

tip

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.