Dart Isolates
last modified May 29, 2026
Bidirectional Communication
When a worker isolate needs to reply to the main isolate, both sides must
exchange their own ports. The worker creates a ReceivePort, sends
its SendPort as the first message, and the main isolate receives
it. This establishes a two-way channel. The following example implements the
ping-pong pattern: the main isolate sends 'ping' and the worker
echoes back 'pong'.
import 'dart:isolate';
void worker(SendPort mainPort) {
final workerPort = ReceivePort();
mainPort.send(workerPort.sendPort); // handshake: send our port back
workerPort.listen((msg) {
print('Worker received: $msg');
mainPort.send('pong'); // reply to main
workerPort.close();
});
}
void main() async {
final mainPort = ReceivePort();
await Isolate.spawn(worker, mainPort.sendPort);
final stream = mainPort.asBroadcastStream();
final SendPort workerPort = await stream.first; // receive worker's port
workerPort.send('ping');
final reply = await stream.first;
print('Main received: $reply'); // Main received: pong
mainPort.close();
}
asBroadcastStream is used because two separate await
expressions read from the same stream: the first value is the worker's
SendPort from the handshake, and the second is the actual reply.
Sending Complex Data
Ports support primitives, List, Map,
Record, and a handful of other built-in types. Instances of custom
classes cannot be sent directly — the Dart VM cannot serialize them across an
isolate boundary. The standard workaround is to convert the object to a map or
record before sending and reconstruct it on the receiving side.
import 'dart:isolate';
void worker(SendPort port) {
// Custom objects cannot cross isolate boundaries directly.
// Encode as a Map instead and reconstruct on the other side.
final data = {
'name': 'Alice',
'scores': [95, 87, 92],
'meta': {'level': 3, 'active': true},
};
port.send(data);
}
void main() async {
final receivePort = ReceivePort();
await Isolate.spawn(worker, receivePort.sendPort);
final Map data = await receivePort.first;
print('Name: ${data['name']}'); // Name: Alice
print('Scores: ${data['scores']}'); // Scores: [95, 87, 92]
receivePort.close();
}
The received map is a deep copy of the original. Mutations on it do not affect the data in the worker isolate — this copy-on-send rule applies to every message passed between isolates.
Isolate Termination
An isolate can be stopped in three ways. Isolate.kill() interrupts
it from the outside with no opportunity for cleanup. The onExit
port receives a message when the isolate finishes naturally, which is useful
for lifecycle tracking. Isolate.exit(port, value) lets the isolate
send a final value and terminate immediately; the object is transferred without
an extra copy, making it more efficient than a send() followed by
a normal return.
import 'dart:isolate';
void worker(SendPort port) {
final result = List.generate(5, (i) => i * i); // [0,1,4,9,16]
// Isolate.exit sends the value and terminates without an extra copy.
Isolate.exit(port, result);
}
void main() async {
final dataPort = ReceivePort();
final exitPort = ReceivePort();
await Isolate.spawn(worker, dataPort.sendPort,
onExit: exitPort.sendPort);
final result = await dataPort.first;
print('Result: $result'); // Result: [0, 1, 4, 9, 16]
await exitPort.first; // wait until the isolate is gone
print('Isolate has exited.');
dataPort.close();
exitPort.close();
}
The onExit port delivers null unless a custom exit
value is configured. In the example above the data message is already sent via
Isolate.exit, so exitPort receives null
as the termination signal.
Error Handling
When an unhandled exception occurs inside an isolate, the isolate terminates
but the rest of the application keeps running. Without an onError
port that failure would go unnoticed. When the port is set, a message arrives
as a two-element list: the first element is the error description string and
the second is the stack trace as text.
import 'dart:isolate';
void worker(SendPort port) {
port.send('starting');
throw StateError('something went wrong'); // unhandled exception
}
void main() async {
final dataPort = ReceivePort();
final errPort = ReceivePort();
await Isolate.spawn(worker, dataPort.sendPort,
onError: errPort.sendPort);
dataPort.listen((msg) => print('Data: $msg'));
errPort.listen((err) {
// err arrives as [errorMessage, stackTrace]
print('Caught: ${(err as List)[0]}');
dataPort.close();
errPort.close();
});
await Future.delayed(Duration(seconds: 1));
}
Setting onError is especially important in production code, where
silent failures can cause hard-to-trace bugs. An error caught through the port
cannot bring down the whole application.
Parallel List Processing
The following example demonstrates the basic map-and-aggregate pattern for parallel data processing. A list of one thousand numbers is split into four equal partitions. Each partition is handed to its own isolate, which computes a partial sum and sends it back. The main isolate collects all partial results and adds them together. The sum of the numbers from 1 to 1000 is 500,500.
import 'dart:isolate';
// Worker receives [SendPort, List<int>] and replies with the partition sum.
void partitionWorker(List<dynamic> args) {
final SendPort port = args[0];
final List<int> nums = List<int>.from(args[1]);
final int sum = nums.fold(0, (acc, x) => acc + x);
port.send(sum);
}
Future<int> parallelSum(List<int> numbers, int workerCount) async {
final size = (numbers.length / workerCount).ceil();
final receivePort = ReceivePort();
int spawned = 0;
for (int i = 0; i < numbers.length; i += size) {
final end = (i + size).clamp(0, numbers.length);
await Isolate.spawn(
partitionWorker, [receivePort.sendPort, numbers.sublist(i, end)]);
spawned++;
}
int total = 0;
int received = 0;
await for (final msg in receivePort) {
total += msg as int;
if (++received == spawned) break; // all partitions collected
}
receivePort.close();
return total;
}
void main() async {
final numbers = List<int>.generate(1000, (i) => i + 1); // 1..1000
final result = await parallelSum(numbers, 4);
print('Sum 1..1000 = $result'); // Sum 1..1000 = 500500
}
In practice the worker count can be set to Platform.numberOfProcessors
to maximise parallelism without unnecessary spawning overhead.
Source
Dart Isolate class, Dart concurrency documentation
This tutorial covered bidirectional isolate communication, sending complex data
across isolate boundaries, graceful termination with Isolate.exit
and the onExit port, error handling via onError, and
a practical parallel list-processing pattern.
Author
List all Dart tutorials.