Isolate #
dart:isolate is Dart’s built-in library providing low-level primitives for Isolate-based concurrency. Each Isolate runs on its own thread with a separate memory heap — no shared state between Isolates, all communication happens through message passing via SendPort and ReceivePort. This article covers the dart:isolate API in depth: how to create Isolates, two-way communication, cross-Isolate error handling, and patterns used in real production code.
This article covers dart:isolate from the Standard Library perspective — the low-level API. For a general overview of when to use Isolates vs async/await, see the Multi Threading article in the Advanced section.The Isolate Memory Model #
flowchart LR
subgraph "Main Isolate"
MI["Event Loop\nHeap A\nDart Objects"]
RPA["ReceivePort A"]
end
subgraph "Worker Isolate"
WI["Event Loop\nHeap B\n(separate)"]
RPB["ReceivePort B"]
end
MI -->|"SendPort.send(msg)\n(message is copied)"| RPB
WI -->|"SendPort.send(result)\n(message is copied)"| RPA
note["❌ No shared memory\n✅ All communication via messages"]ReceivePort and SendPort
#
ReceivePort is the message-receiving endpoint. Every ReceivePort has a sendPort that can be given to other parties to send messages:
import 'dart:isolate';
Future<void> main() async {
// Create a ReceivePort — an endpoint for receiving messages
final receivePort = ReceivePort();
// sendPort is the "address" that can be sent to another Isolate
final SendPort sendPort = receivePort.sendPort;
// Send a message to ourselves (for demonstration)
sendPort.send('Halo!');
sendPort.send(42);
sendPort.send({'kunci': 'nilai'});
sendPort.send(null);
// Read messages — a ReceivePort is a Stream
int hitungan = 0;
await for (final pesan in receivePort) {
print('Received: $pesan');
hitungan++;
if (hitungan >= 4) break; // stop after 4 messages
}
receivePort.close(); // must close to stop the event loop
}
Data Types That Can Be Sent #
import 'dart:isolate';
import 'dart:typed_data';
final port = ReceivePort();
final send = port.sendPort;
// ✓ CAN be sent between Isolates:
send.send(null); // null
send.send(true); // bool
send.send(42); // int
send.send(3.14); // double
send.send('teks'); // String
send.send([1, 2, 3]); // List (copied)
send.send({'a': 1}); // Map (copied)
send.send({1, 2, 3}); // Set (copied)
send.send(Uint8List.fromList([1, 2, 3])); // TypedData (copied)
send.send(port.sendPort); // SendPort ✓
send.send( // TransferableTypedData (zero-copy)
TransferableTypedData.fromList([Uint8List(100)])
);
// ✗ CANNOT be sent:
// send.send(File('test.txt')); // native resource objects
// send.send(Socket(...)); // native sockets
// send.send(() => print('hi')); // closures/functions (except top-level)
// send.send(Stream.empty()); // Streams
Isolate.run — the Easiest Way (Dart 2.19+)
#
import 'dart:isolate';
// Isolate.run — create, run, return the result, clean up automatically
Future<void> main() async {
// The function run in a new Isolate
// Must be top-level or static (can't be a closure that captures state)
final hasil = await Isolate.run(() {
// This code runs in a separate Isolate
int total = 0;
for (int i = 0; i < 10000000; i++) {
total += i;
}
return total;
});
print('Total: $hasil'); // 49999995000000
}
// With arguments — send data via the closure
Future<List<int>> sortkanDiBackground(List<int> data) async {
return await Isolate.run(() {
// data is copied into this Isolate
final salinan = List<int>.from(data);
salinan.sort();
return salinan;
});
}
// Errors in Isolate.run — propagate to the caller
Future<void> contohError() async {
try {
await Isolate.run(() {
throw Exception('Error in the Isolate!');
});
} catch (e) {
print('Error caught in main: $e');
// Exceptions from Isolate.run are wrapped as RemoteError
}
}
Isolate.spawn — Full Control
#
Isolate.spawn creates a persistent Isolate that can receive many messages:
import 'dart:isolate';
// The entry point MUST be a top-level or static function
void workerEntryPoint(SendPort kirimKeMain) {
final terimaPerintah = ReceivePort();
// Send our SendPort to main so main can send commands
kirimKeMain.send(terimaPerintah.sendPort);
// Process incoming commands
terimaPerintah.listen((pesan) {
if (pesan == null) {
// Signal to close
terimaPerintah.close();
return;
}
final int angka = pesan as int;
final hasil = _hitungPrime(angka); // heavy computation
kirimKeMain.send(hasil);
});
}
bool _hitungPrime(int n) {
if (n < 2) return false;
for (int i = 2; i <= n ~/ 2; i++) {
if (n % i == 0) return false;
}
return true;
}
Future<void> main() async {
final terimaMain = ReceivePort();
// Spawn the worker Isolate
final isolate = await Isolate.spawn(
workerEntryPoint,
terimaMain.sendPort,
);
// Receive the SendPort from the worker
final SendPort kirimKeWorker = await terimaMain.first;
// Send several jobs
final hasilStream = terimaMain.skip(1); // skip the SendPort already taken
final iterator = hasilStream.iterator;
for (final n in [97, 100, 101, 200, 997]) {
kirimKeWorker.send(n);
await iterator.moveNext();
final isPrime = iterator.current as bool;
print('$n is a prime number: $isPrime');
}
// Send the close signal
kirimKeWorker.send(null);
terimaMain.close();
// Kill the Isolate if it's still alive
isolate.kill(priority: Isolate.beforeNextEvent);
}
Two-Way Communication — a Robust Pattern #
For reliable two-way communication, use a pattern with correlation IDs:
import 'dart:isolate';
import 'dart:async';
// Request message from main to worker
class Request {
final int id;
final dynamic data;
final SendPort replyTo;
const Request({required this.id, required this.data, required this.replyTo});
}
// Response message from worker to main
class Response {
final int id;
final dynamic hasil;
final String? error;
const Response({required this.id, this.hasil, this.error});
}
// A reliable worker with ID tracking
void workerRobust(SendPort kirimKeMain) {
final terima = ReceivePort();
kirimKeMain.send(terima.sendPort);
terima.listen((pesan) {
if (pesan is Request) {
try {
final hasil = _prosesData(pesan.data);
pesan.replyTo.send(Response(id: pesan.id, hasil: hasil));
} catch (e) {
pesan.replyTo.send(Response(id: pesan.id, error: e.toString()));
}
}
});
}
dynamic _prosesData(dynamic data) {
// Simulated computation
return 'Hasil dari: $data';
}
// A thread-safe client
class IsolateClient {
final SendPort _kirimKeWorker;
final ReceivePort _terima;
final Map<int, Completer<dynamic>> _pending = {};
int _idCounter = 0;
IsolateClient._(this._kirimKeWorker, this._terima) {
_terima.listen((pesan) {
if (pesan is Response) {
final completer = _pending.remove(pesan.id);
if (pesan.error != null) {
completer?.completeError(Exception(pesan.error));
} else {
completer?.complete(pesan.hasil);
}
}
});
}
static Future<IsolateClient> buat() async {
final terima = ReceivePort();
await Isolate.spawn(workerRobust, terima.sendPort);
final SendPort kirimKeWorker = await terima.first;
return IsolateClient._(kirimKeWorker, terima);
}
Future<dynamic> kirim(dynamic data, {Duration timeout = const Duration(seconds: 30)}) {
final id = _idCounter++;
final completer = Completer<dynamic>();
_pending[id] = completer;
_kirimKeWorker.send(Request(
id: id,
data: data,
replyTo: _terima.sendPort,
));
return completer.future.timeout(
timeout,
onTimeout: () {
_pending.remove(id);
throw TimeoutException('Request $id timed out after ${timeout.inSeconds}s');
},
);
}
void tutup() {
_terima.close();
for (final c in _pending.values) {
c.completeError(StateError('Client closed'));
}
_pending.clear();
}
}
// Usage
Future<void> main() async {
final client = await IsolateClient.buat();
// Send many requests in parallel
final futures = [
client.kirim('data A'),
client.kirim('data B'),
client.kirim('data C'),
];
final hasil = await Future.wait(futures);
print(hasil); // ['Hasil dari: data A', ...]
client.tutup();
}
Cross-Isolate Error Handling #
import 'dart:isolate';
Future<void> main() async {
final terimaError = ReceivePort();
final terimaExit = ReceivePort();
final terimaHasil = ReceivePort();
final isolate = await Isolate.spawn(
workerDenganError,
terimaHasil.sendPort,
// Error listener — catch uncaught errors in the Isolate
onError: terimaError.sendPort,
// Exit listener — notification when the Isolate finishes
onExit: terimaExit.sendPort,
// Errors DON'T kill the Isolate (default: true = kills it)
errorsAreFatal: false,
);
// Listen for errors
terimaError.listen((error) {
// error is a List: [errorMessage, stackTrace]
final List<dynamic> errorList = error as List<dynamic>;
print('Error in the Isolate: ${errorList[0]}');
print('Stack trace: ${errorList[1]}');
});
// Listen for exit
terimaExit.first.then((_) {
print('Isolate finished');
terimaError.close();
terimaExit.close();
terimaHasil.close();
});
// Listen for results
terimaHasil.listen((pesan) {
print('Result: $pesan');
});
// Control from outside
// Add an error listener to an already running Isolate
isolate.addErrorListener(terimaError.sendPort);
isolate.removeErrorListener(terimaError.sendPort);
// Pause and resume
final capability = isolate.pause();
await Future.delayed(Duration(seconds: 1));
isolate.resume(capability);
// Send a kill signal
// isolate.kill(priority: Isolate.immediate); // immediately
// isolate.kill(priority: Isolate.beforeNextEvent); // after the current event
}
void workerDenganError(SendPort kirimHasil) {
kirimHasil.send('Started working');
try {
kirimHasil.send('Step 1 done');
throw Exception('Simulated error');
kirimHasil.send('This is never reached');
} catch (e) {
kirimHasil.send('Error caught locally: $e');
}
// An uncaught error will be sent to the onError port
throw StateError('Uncaught error!');
}
RawReceivePort — High Performance
#
RawReceivePort is a lower-level version of ReceivePort without the Stream overhead:
import 'dart:isolate';
Future<void> main() async {
// RawReceivePort — no Stream overhead, direct callback
final rawPort = RawReceivePort((pesan) {
print('Received (raw): $pesan');
});
rawPort.sendPort.send('Halo');
rawPort.sendPort.send(42);
await Future.delayed(Duration(milliseconds: 100));
// Change the handler
rawPort.handler = (pesan) {
print('New handler: $pesan');
};
rawPort.sendPort.send('New message');
await Future.delayed(Duration(milliseconds: 100));
rawPort.close(); // must close
}
Isolate Pools — Reuse for Performance #
Creating a new Isolate per request has significant overhead. For frequent tasks, build a pool:
import 'dart:isolate';
import 'dart:async';
import 'dart:collection';
typedef IsolateTask<T> = Future<T> Function();
class IsolatePool {
final int ukuran;
final _workers = <_WorkerIsolate>[];
final _antrian = Queue<_PendingTask>();
bool _ditutup = false;
IsolatePool(this.ukuran);
Future<void> inisialisasi() async {
for (int i = 0; i < ukuran; i++) {
final worker = await _WorkerIsolate.buat();
_workers.add(worker);
}
}
Future<T> jalankan<T>(dynamic Function() tugas) async {
if (_ditutup) throw StateError('Pool is closed');
final completer = Completer<T>();
final pending = _PendingTask(tugas, completer as Completer<dynamic>);
// Find a free worker
final workerKosong = _workers.firstWhere(
(w) => !w.sibuk,
orElse: () => throw StateError('No worker available'),
);
if (!workerKosong.sibuk) {
await workerKosong.jalankan(pending);
} else {
_antrian.addLast(pending);
}
return completer.future as Future<T>;
}
Future<void> tutup() async {
_ditutup = true;
for (final worker in _workers) {
await worker.tutup();
}
}
}
class _PendingTask {
final dynamic Function() tugas;
final Completer<dynamic> completer;
_PendingTask(this.tugas, this.completer);
}
class _WorkerIsolate {
bool sibuk = false;
late final SendPort _kirimKeWorker;
late final ReceivePort _terima;
late final Isolate _isolate;
static Future<_WorkerIsolate> buat() async {
final worker = _WorkerIsolate();
worker._terima = ReceivePort();
worker._isolate = await Isolate.spawn(
_workerLoop,
worker._terima.sendPort,
);
worker._kirimKeWorker = await worker._terima.first;
return worker;
}
Future<void> jalankan(_PendingTask task) async {
sibuk = true;
_kirimKeWorker.send(task.tugas);
final hasil = await _terima.first;
task.completer.complete(hasil);
sibuk = false;
}
Future<void> tutup() async {
_kirimKeWorker.send(null); // close signal
_terima.close();
_isolate.kill();
}
}
void _workerLoop(SendPort kirimKeMain) {
final terima = ReceivePort();
kirimKeMain.send(terima.sendPort);
terima.listen((tugas) {
if (tugas == null) {
terima.close();
return;
}
final hasil = (tugas as dynamic Function())();
kirimKeMain.send(hasil);
});
}
// Pool usage
Future<void> main() async {
final pool = IsolatePool(4); // 4 worker Isolates
await pool.inisialisasi();
// Send many tasks in parallel
final futures = List.generate(
10,
(i) => pool.jalankan<int>(() {
// Simulated heavy computation
int sum = 0;
for (int j = 0; j < 1000000; j++) sum += j;
return sum + i;
}),
);
final hasil = await Future.wait(futures);
print('Result: ${hasil.length} tasks completed');
await pool.tutup();
}
Capabilities #
Capability is a unique token used for Isolate access control — pause and resume:
import 'dart:isolate';
Future<void> main() async {
final isolate = await Isolate.spawn(
(SendPort _) { /* ... */ },
ReceivePort().sendPort,
);
// Pause returns a Capability as the "key"
final pauseCapability = isolate.pause();
print('Isolate paused');
await Future.delayed(Duration(seconds: 2));
// Resume can only be done by whoever holds the Capability
isolate.resume(pauseCapability);
print('Isolate resumed');
// Capabilities can also be created independently
final cap1 = Capability();
final cap2 = Capability();
print(cap1 == cap2); // false — every Capability is unique
print(cap1 == cap1); // true — exactly the same
}
Comparison: Isolate.run vs Isolate.spawn vs compute
#
| Aspect | Isolate.run | Isolate.spawn | compute (Flutter) |
|---|---|---|---|
| Available in | Dart 2.19+ | All versions | Flutter only |
| Lifecycle | Automatic (one task) | Manual | Automatic |
| Communication | One return value | Multi-message | One return value |
| Error handling | Via try/catch | Manual | Via try/catch |
| Boilerplate | Minimal | Lots | Minimal |
| Best for | A single computation | Persistent workers | Flutter (like Isolate.run) |
dart:isolate Anti-Patterns
#
Not Closing ReceivePorts #
// ANTI-PATTERN: ReceivePort not closed — the event loop can't stop!
Future<void> buruk() async {
final rp = ReceivePort();
await Isolate.spawn(worker, rp.sendPort);
final hasil = await rp.first;
// ✗ rp is not closed — the program will never finish!
print(hasil);
}
// CORRECT: always close the ReceivePort when done
Future<void> baik() async {
final rp = ReceivePort();
try {
await Isolate.spawn(worker, rp.sendPort);
final hasil = await rp.first;
print(hasil);
} finally {
rp.close(); // ✓ always closed
}
}
Sending Objects That Can’t Be Copied #
// ANTI-PATTERN: sending non-serializable objects
class ProdukDenganCallback {
final String nama;
final void Function() onUpdate; // callback/closure
ProdukDenganCallback(this.nama, this.onUpdate);
}
sendPort.send(ProdukDenganCallback('Laptop', () {}));
// ✗ Illegal argument in isolate message: closure
// CORRECT: send only serializable data
sendPort.send({'nama': 'Laptop', 'harga': 15000000}); // ✓ Maps can be sent
Summary #
- Isolates don’t share memory — all communication happens through copied messages. No race conditions, no mutexes, but also no shared state.
Isolate.runfor a single heavy computation — create, run, return the result, clean up automatically. The simplest API for Dart 2.19+.Isolate.spawnfor persistent workers — Isolates that receive many messages over their lifetime. NeedsSendPort/ReceivePortfor two-way communication.- Entry points must be top-level or static — closures capturing outer variables can’t be sent as Isolate entry points.
- Always close
ReceivePorts withclose()— open ports prevent the event loop from stopping, so the program never finishes.- The
onErrorport catches uncaught exceptions in an Isolate — without it, errors in the Isolate just kill that Isolate without any notification.errorsAreFatal: falseso errors in an Isolate don’t immediately kill it — useful for workers that must stay alive despite occasional errors.- Use Completer + Map for tracking async request-response — correlation IDs ensure responses are paired with the right requests.
RawReceivePortfor high throughput — without Dart Stream overhead, the handler is called directly when a message arrives.- Isolate pools for frequent tasks — spawn once, reuse many times. The ~5ms overhead of spawning a new Isolate is very noticeable for thousands of small tasks.