In Dart, when would you use Isolate.run and when Isolate.spawn, for example to hash thousands of files in a backup CLI?
answer
- one computation versus a lasting worker
- Isolate.run returns a Future of the result
- spawn takes an entry point and message
- errors and exit handled for you
- closures capture more than you think
basics
~20 sIsolate.run starts an isolate, runs one function, returns its result as a Future, rethrows its error, and exits. Isolate.spawn starts a long-lived worker from an entry point and a message, and you manage ports, results, errors and shutdown yourself.
solid answer
~50 s`Isolate.run(computation)` (Dart 2.19+) is the default: it spawns an isolate, runs the function, returns a `Future` of its result — handed back via `Isolate.exit`, without copying — rethrows the function's error in the caller, and shuts the isolate down. It is ideal for a bounded job such as hashing one batch of files. `Isolate.spawn(entryPoint, message, ...)` starts an isolate that runs `entryPoint(message)` and can stay alive, typically receiving work over ports; you wire results, errors (`onError`), exit (`onExit`) and shutdown (`kill`) yourself. For a backup CLI, split the file list into a few batches — around `Platform.numberOfProcessors` — and run each with `Isolate.run`, rather than one isolate per file. Reach for `spawn` when a worker must keep state between many requests. Either way, pass only what the worker needs: a closure can capture surrounding state that must be copied, or cannot be sent at all.
code
dart · 23 linesimport 'dart:io';
import 'dart:isolate';
import 'dart:math';
int checksum(List<int> bytes) =>
bytes.fold(0, (hash, b) => (hash * 31 + b) & 0xffffffff);
// Top-level helper: the closure captures only `paths`.
Future<Map<String, int>> hashBatch(List<String> paths) => Isolate.run(
() => {for (final p in paths) p: checksum(File(p).readAsBytesSync())},
debugName: 'hash-batch',
);
Future<Map<String, int>> hashFiles(List<String> files) async {
if (files.isEmpty) return {};
final size = (files.length / Platform.numberOfProcessors).ceil();
final batches = [
for (var i = 0; i < files.length; i += size)
files.sublist(i, min(i + size, files.length)),
];
final parts = await Future.wait(batches.map(hashBatch));
return {for (final part in parts) ...part};
}go deeper
Remember that Isolate.run is for one computation that returns a result, and Isolate.spawn is for a worker that stays alive.
Explain what Isolate.run automates — result via exit, error rethrow, shutdown — and what you must wire yourself with spawn.
Size work to the core count, batch to amortise spawn cost, and avoid closure capture of unsendable or large state.
Choose between one-shot isolates and a persistent worker pool from the workload's shape, and hide that choice behind a small async API.
## Two ways to start an isolate `dart:isolate` offers two everyday entry points, plus `spawnUri` for special cases. | | `Isolate.run` | `Isolate.spawn` | |---|---|---| | Signature | `Future<R> run<R>(FutureOr<R> computation(), {String? debugName})` | `Future<Isolate> spawn<T>(void entryPoint(T message), T message, {paused, errorsAreFatal, onExit, onError, debugName})` | | Lifetime | One computation, then the isolate exits | As long as the entry point keeps it alive, usually by listening on a port | | Result | The `Future` completes with the return value | You send results back yourself | | Errors | Rethrown in the caller; uncaught async errors arrive as `RemoteError` | Only if you pass an `onError` port or listen to `errors` | | Shutdown | Automatic | The isolate exits when it has nothing left to do, calls `Isolate.exit`, or is killed | | Since | Dart 2.19 | Long-standing; closures accepted as entry points since 2.15 | ## Isolate.run: the default Under the hood, `Isolate.run` does what you would otherwise write by hand: it creates a receive port, spawns an isolate with that port registered for `onError` and `onExit`, runs the computation there, and sends the result back with `Isolate.exit`, which hands the object graph over without copying. If the computation throws, the caller's `await` throws the same error type with the remote stack trace; if the isolate dies without a result, the future fails with a `RemoteError`. The dart.dev guidance is that `Isolate.run` is the recommended API in most cases. ## Isolate.spawn: a lasting worker `Isolate.spawn` is for workers that should outlive one computation: - a parser that keeps a large lookup table loaded and answers many requests; - a database or compression worker fed a stream of jobs; - a pool of workers you reuse to avoid spawning repeatedly. The entry point receives exactly one `message` — typically containing a `SendPort` so the worker can reply. The request/response protocol over ports is its own topic; what matters here is that everything `Isolate.run` did for you — result delivery, error propagation, shutdown — becomes your responsibility. ## The backup CLI Hashing ten thousand files is CPU-bound and embarrassingly parallel. A sensible design: 1. Read the file list on the main isolate. 2. Split it into `Platform.numberOfProcessors` batches. 3. Run each batch with `Isolate.run`, and `Future.wait` for all of them. 4. Merge the resulting maps. Why not one `Isolate.run` per file? Each isolate costs startup time and memory (dart:isolate documents a base overhead in the order of 30 kb), and each result is a separate message. Batching amortises that. If the tool ran as a daemon receiving files continuously, a small pool of `spawn`ed workers fed over ports would be the better shape. ## The closure trap Both APIs accept closures, and a closure carries its enclosing context. The SDK warns that the VM may capture **more** variables than the closure uses. If that context includes something unsendable — an open `RandomAccessFile`, a `Socket` — the call fails at run time with an illegal-argument error; if it includes a large object graph, that graph is copied into the worker. The fix is to call `Isolate.run` from a small function that receives only the data it needs as parameters, or to pass a top-level or static function. ## spawnUri, briefly `Isolate.spawnUri(uri, args, message)` starts a separate program from a Dart source or snapshot URI. It is slower, the new isolate does not share its spawner's code, and it is rarely what an app needs.
- What does the caller see if the function passed to Isolate.run throws a FileSystemException?The future returned by `Isolate.run` completes with that error, so `await` throws a `FileSystemException` in the caller, with the stack trace from the worker. Only uncaught asynchronous errors inside the worker, which reach it through the error listener, arrive as a `RemoteError` instead of the original type.
- Why might Isolate.run fail with an illegal-argument error even though the function only uses a list of paths?The closure's captured context can include more than the variables it references, such as an open file or socket from the enclosing function. Those cannot be sent between isolates, so spawning fails. Move the `Isolate.run` call into a function whose parameters are exactly the data needed.
saying these in an interview costs you the question
- Isolate.run keeps the isolate alive for later calls.
- Isolate.spawn returns the entry point's result as a Future.
- Errors thrown inside Isolate.run are silently lost.
- Spawning one isolate per file maximises throughput.
- A closure passed to Isolate.run captures only the variables it uses.