On this page23 sections
- Prerequisites
- Connection With Previous Lectures
- Learning Objectives
- 1. What Is an Isolate?
- 2. You Are Already in an Isolate
- 3. The Problem: Heavy Work Blocks main
- 4. Isolate.run: Move the Work Out
- 5. Passing Data In, Getting a Result Out
- 6. Values Are Copied
- 7. No Shared State, Not Even Globals
- 8. Errors Come Back to main
- 9. What Cannot Be Sent
- 10. Cost vs Overhead
- 11. A Real-World Example
- Code Examples
- Common Beginner Mistakes
- Best Practices
- Mini Challenge
- Practice Questions
- Practice Answers
- Quick Revision
- Key Takeaways
- What's Next?
Lecture 98 — Isolates
| Category | 16 — Concurrency & Isolates |
| Lecture | 098 of 140 |
| Estimated video length | 10 min |
| Target | Dart 3.12.2 |
| Level | Intermediate |
A heavy calculation freezes the whole program, because
asynconly helps with waiting. An isolate runs the calculation somewhere else, somainkeeps responding.
Contents
Prerequisites · Connection · Objectives · Lesson · Code Examples · Common Mistakes · Best Practices · Mini Challenge · Practice Questions · Answers · Takeaways
Every output and error message in this lecture is real. Each one was produced by running the code shown on Dart 3.12.2. Timing lines are approximate:
tick at 402 msordone at 811 mswill show different numbers on every run and every computer. Read them as "about 0.4 s" and compare their pattern, not the exact digits.
Prerequisites
Students should already understand, from Lectures 087–097:
Stopwatchand blocking code (Lecture 087)Future,async, andawait(Lectures 088–089)try/catcharoundawait(Lecture 090)- records like
(total, largest)and destructuring them (Lecture 091) - the single thread and its event loop (Lectures 087 and 097)
Connection With Previous Lectures
Lecture 087 ended with a warning: a busy loop inside a delayed task still blocked every task queued behind it. Lecture 097 explained why — your code runs on one thread, and the event loop can only pick the next task when the current one finishes.
Waiting for a network reply is fine: the thread is free while it waits. Counting millions of primes is working, and nothing else can run — in an app, a frozen screen. We need a second place where Dart can run code.
Learning Objectives
By the end of this lecture you will be able to:
- Explain what an isolate is: its own memory and its own event loop
- Recognize that every Dart program already runs in a
mainisolate - Run heavy work in a new isolate with
Isolate.run - Prove that
mainstays responsive while an isolate works - Pass data into an isolate and get a result back
- Show that values are copied, so isolates share no state
- Recognize an object that cannot be sent to an isolate
- Decide when the cost of starting an isolate is worth it
1. What Is an Isolate?
An isolate is an independent worker that runs Dart code. Each isolate has:
- its own memory — its own variables and objects
- its own event loop — its own single thread running its own tasks
Isolates do not share memory. The only way to get something from one isolate to another is to send a copy of it.
Analogy — two cooks in two separate kitchens. Each has their own ingredients and works at full speed without bumping into the other. To share something, one cook passes a copy of the recipe card through a hatch; the original stays in their own kitchen.
2. You Are Already in an Isolate
Every Dart program starts in one isolate, named main. The dart:isolate library lets you
ask which one is running:
import 'dart:isolate';
void main() {
print('Running in: ${Isolate.current.debugName}');
}Running in: mainEverything you have written so far ran inside this single isolate.
3. The Problem: Heavy Work Blocks main
countPrimes counts the prime numbers up to a limit. It is pure calculation — no waiting.
Timer.periodic (from dart:async) runs a function every 200 ms; think of it as a screen
animation that should never stop:
import 'dart:async';
int countPrimes(int limit) {
var count = 0;
for (var n = 2; n <= limit; n++) {
var isPrime = true;
for (var d = 2; d * d <= n; d++) {
if (n % d == 0) {
isPrime = false;
break;
}
}
if (isPrime) count++;
}
return count;
}
void main() async {
var clock = Stopwatch()..start();
var ticker = Timer.periodic(Duration(milliseconds: 200), (_) {
print('tick at ${clock.elapsedMilliseconds} ms');
});
var count = countPrimes(5000000);
print('$count primes, done at ${clock.elapsedMilliseconds} ms');
await Future.delayed(Duration(milliseconds: 300));
ticker.cancel();
}One run printed:
348513 primes, done at 805 ms
tick at 807 ms
tick at 1001 msTicks were due at 200, 400, and 600 ms. None appeared until the counting finished. The
"animation" froze for 0.8 seconds. ticker.cancel() stops the timer.
4. Isolate.run: Move the Work Out
Isolate.run starts a new isolate, runs a function there, returns its result as a Future,
and shuts the isolate down. Same countPrimes, one changed line:
import 'dart:async';
import 'dart:isolate';
int countPrimes(int limit) {
var count = 0;
for (var n = 2; n <= limit; n++) {
var isPrime = true;
for (var d = 2; d * d <= n; d++) {
if (n % d == 0) {
isPrime = false;
break;
}
}
if (isPrime) count++;
}
return count;
}
void main() async {
var clock = Stopwatch()..start();
var ticker = Timer.periodic(Duration(milliseconds: 200), (_) {
print('tick at ${clock.elapsedMilliseconds} ms');
});
var count = await Isolate.run(() => countPrimes(5000000));
ticker.cancel();
print('$count primes, done at ${clock.elapsedMilliseconds} ms');
}One run printed:
tick at 203 ms
tick at 402 ms
tick at 602 ms
tick at 802 ms
348513 primes, done at 811 msNow the ticks arrive on time. The work took about as long, but main was free: await
waited for the result, and the event loop kept running the timer.
5. Passing Data In, Getting a Result Out
The function you give Isolate.run takes no parameters. To pass data in, use variables in
the function body — Dart sends them along. Whatever the function returns comes back:
import 'dart:isolate';
int fib(int n) => n < 2 ? n : fib(n - 1) + fib(n - 2);
void main() async {
var n = 40;
var clock = Stopwatch()..start();
var result = await Isolate.run(() => fib(n));
print('fib($n) = $result in ${clock.elapsedMilliseconds} ms');
}fib(40) = 102334155 in 757 msn travels into the new isolate; 102334155 travels back. This recursive fib
(recursion was Lecture 038) repeats work endlessly — perfect heavy work.
6. Values Are Copied
When marks is sent to the isolate, the isolate receives a copy. Changing the copy does
not touch the original:
import 'dart:isolate';
void main() async {
var marks = [80, 75, 85];
var result = await Isolate.run(() {
marks.add(100);
marks[0] = 0;
return marks;
});
print('Inside isolate: $result');
print('Back in main: $marks');
}Inside isolate: [0, 75, 85, 100]
Back in main: [80, 75, 85]The returned list is a copy too. Two isolates can never trip over the same object.
7. No Shared State, Not Even Globals
A top-level variable lives in one isolate's memory. The new isolate gets its own copy:
import 'dart:isolate';
int counter = 0;
void main() async {
await Isolate.run(() {
counter = 99;
print('Isolate sees counter = $counter');
});
print('Main sees counter = $counter');
}Isolate sees counter = 99
Main sees counter = 0To get information back to main, return it.
8. Errors Come Back to main
If the function throws, the future from Isolate.run fails with the same error, so
try/catch around await works as in Lecture 090:
import 'dart:isolate';
void main() async {
try {
await Isolate.run(() => throw FormatException('Bad mark: abc'));
} on FormatException catch (e) {
print('Caught in main: $e');
}
}Caught in main: FormatException: Bad mark: abc9. What Cannot Be Sent
Numbers, strings, lists, maps, records, and your own plain objects can all be copied. An
object tied to a resource of this isolate cannot — for example an open file handle
(openSync from dart:io):
import 'dart:io';
import 'dart:isolate';
void main() async {
var file = File('notes.txt')..writeAsStringSync('hello');
var handle = file.openSync();
try {
await Isolate.run(() => handle.lengthSync());
} catch (e) {
print(e);
}
handle.closeSync();
}Invalid argument(s): Illegal argument in isolate message: object is unsendable - Library:'dart:io' Class: _RandomAccessFileOpsImpl@16069316 (see restrictions listed at `SendPort.send()` documentation for more information)
<- _ops in Instance of '_RandomAccessFile' (from dart:io)
<- field handle in main.<anonymous closure> (from file:///…/main.dart)
<- computation in Instance of '_RemoteRunner<int>' (from dart:isolate)(The file path is shortened.) Read the <- lines bottom-up: the closure used handle, and
handle holds an open file. Send the file name instead and open it inside the isolate.
We will study files in Lecture 106 — File Handling.
10. Cost vs Overhead
Starting an isolate, copying data in, and copying the result back takes time. For tiny work, that overhead is bigger than the work itself:
import 'dart:isolate';
int total(List<int> prices) => prices.fold(0, (sum, p) => sum + p);
void main() async {
var prices = [500, 1200, 300];
var clock = Stopwatch()..start();
var direct = total(prices);
print('Main thread: $direct in ${clock.elapsedMicroseconds} µs');
clock.reset();
var viaIsolate = await Isolate.run(() => total(prices));
print('Isolate.run: $viaIsolate in ${clock.elapsedMicroseconds} µs');
}Main thread: 2000 in 222 µs
Isolate.run: 2000 in 4936 µsAbout 0.2 ms versus 5 ms — twenty times slower. Use an isolate for work that takes hundreds of milliseconds, like Steps 4 and 5, not for a quick sum.
11. A Real-World Example
Sara's banking app summarizes a billion transactions. The ticker stands in for the app's screen, and the result comes back as a record:
import 'dart:async';
import 'dart:isolate';
// A pretend transaction amount between 0 and 4999 rupees.
int amountOf(int i) => (i * 37) % 5000;
(int, int) summarize(int count) {
var total = 0;
var largest = 0;
for (var i = 0; i < count; i++) {
var amount = amountOf(i);
total += amount;
if (amount > largest) largest = amount;
}
return (total, largest);
}
void main() async {
var clock = Stopwatch()..start();
var ticker = Timer.periodic(Duration(milliseconds: 250), (_) {
print(' screen still responsive (${clock.elapsedMilliseconds} ms)');
});
print('Summarizing Sara\'s transactions...');
try {
var (total, largest) = await Isolate.run(() => summarize(1000000000));
print('Total: $total, largest: $largest');
} catch (e) {
print('Report failed: $e');
} finally {
ticker.cancel();
}
print('Finished at ${clock.elapsedMilliseconds} ms');
}One run printed:
Summarizing Sara's transactions...
screen still responsive (253 ms)
screen still responsive (502 ms)
screen still responsive (752 ms)
screen still responsive (1002 ms)
screen still responsive (1252 ms)
Total: 2499500000000, largest: 4999
Finished at 1283 msData in by closure, a record back, errors caught — and the screen never froze.
Code Examples
Example 1 — A First Isolate.run
import 'dart:isolate';
void main() async {
var sum = await Isolate.run(() {
var total = 0;
for (var i = 1; i <= 100000000; i++) {
total += i;
}
return total;
});
print('Sum: $sum');
}Sum: 5000000050000000Example 2 — Naming the Isolate
import 'dart:isolate';
void main() async {
var name = await Isolate.run(
() => Isolate.current.debugName,
debugName: 'marks-worker',
);
print('Main: ${Isolate.current.debugName}, worker: $name');
}Main: main, worker: marks-workerExample 3 — Passing Two Values In
import 'dart:isolate';
int countMultiples(int limit, int of) {
var count = 0;
for (var i = 1; i <= limit; i++) {
if (i % of == 0) count++;
}
return count;
}
void main() async {
var limit = 50000000;
var of = 7;
var count = await Isolate.run(() => countMultiples(limit, of));
print('$count multiples of $of');
}7142857 multiples of 7Example 4 — Returning a List
import 'dart:isolate';
void main() async {
var squares = await Isolate.run(() => [for (var i = 1; i <= 5; i++) i * i]);
print(squares);
}[1, 4, 9, 16, 25]Example 5 — A Map Is Copied
import 'dart:isolate';
void main() async {
var stock = {'Pen': 10};
await Isolate.run(() => stock['Pen'] = 0);
print(stock);
}{Pen: 10}Example 6 — An Error Crosses Back
import 'dart:isolate';
void main() async {
try {
await Isolate.run(() => throw StateError('Worker failed'));
} catch (e) {
print('Caught: $e');
}
}Caught: Bad state: Worker failedExample 7 — Student Marks
import 'dart:isolate';
void main() async {
var passing = 50;
var passed = await Isolate.run(() {
var count = 0;
for (var i = 0; i < 10000000; i++) {
var mark = i % 101; // pretend marks 0–100
if (mark >= passing) count++;
}
return count;
});
print('$passed of 10000000 students passed');
}5049500 of 10000000 students passedExample 8 — Shopping Total
import 'dart:isolate';
void main() async {
var total = await Isolate.run(() {
var sum = 0;
for (var i = 0; i < 20000000; i++) {
sum += 100 + i % 900; // pretend prices 100–999
}
return sum;
});
print('Total sales: $total');
}Total sales: 10989930000Example 9 — Bank Balance Stays Safe
import 'dart:isolate';
void main() async {
var balance = [1000];
var projected = await Isolate.run(() {
for (var day = 0; day < 365; day++) {
balance[0] += 10;
}
return balance[0];
});
print('Projected: $projected, real: ${balance[0]}');
}Projected: 4650, real: 1000Example 10 — Hottest Temperature
import 'dart:isolate';
double hottestF(int readings) {
var hottest = -1000.0;
for (var i = 0; i < readings; i++) {
var celsius = (i % 450) / 10; // pretend readings 0.0–44.9
var f = celsius * 9 / 5 + 32;
if (f > hottest) hottest = f;
}
return hottest;
}
void main() async {
var hottest = await Isolate.run(() => hottestF(30000000));
print('Hottest: ${hottest.toStringAsFixed(1)}°F');
}Hottest: 112.8°FCommon Beginner Mistakes
1. Expecting a change inside the isolate to reach main
var marks = [80, 75];
await Isolate.run(() => marks.add(90));
print(marks); // still [80, 75]No error — the isolate changed its own copy. Return the new value instead.
2. Forgetting await
import 'dart:isolate';
void main() {
var result = Isolate.run(() => 6 * 7);
print(result);
}Instance of 'Future<int>'Isolate.run returns a Future, like any async function.
3. Using an unsendable object inside the closure
An open file handle fails with object is unsendable (Step 9). Send plain data instead.
4. Using an isolate for tiny work
A quick sum became about twenty times slower (Step 10).
5. Using an isolate to wait
An isolate helps with calculating, not waiting. A Future.delayed or a network call
already leaves main free — just await it.
Best Practices
- Keep
mainfor quick work and waiting; move long calculations toIsolate.run. - Measure with
Stopwatch— an isolate should make the screen smooth, not the program slower. - Pass only the data the job needs; every value is copied in and out.
- Return results instead of changing outside variables.
- Wrap
await Isolate.run(...)intry/catch, like any future. - In Flutter, the
computefunction does the same job asIsolate.run.
Mini Challenge
Ali wants the count of primes up to 8,000,000 and a ticker every 300 ms that proves
main is free. Print the count and the finish time.
Try it yourself before opening the solution.
Show solution
import 'dart:async';
import 'dart:isolate';
bool isPrime(int n) {
for (var d = 2; d * d <= n; d++) {
if (n % d == 0) return false;
}
return n >= 2;
}
void main() async {
var clock = Stopwatch()..start();
var ticker = Timer.periodic(Duration(milliseconds: 300), (_) {
print('tick at ${clock.elapsedMilliseconds} ms');
});
var count = await Isolate.run(() {
var found = 0;
for (var n = 2; n <= 8000000; n++) {
if (isPrime(n)) found++;
}
return found;
});
ticker.cancel();
print('$count primes, done at ${clock.elapsedMilliseconds} ms');
}One run printed:
tick at 303 ms
tick at 601 ms
tick at 901 ms
tick at 1202 ms
tick at 1502 ms
539777 primes, done at 1590 msThe ticks keep their 300 ms rhythm because the counting runs in another isolate.
Practice Questions
Solve each one before looking at the answers.
Beginner
- Print the name of the isolate
mainruns in. - Use
Isolate.runto calculate12 * 12and print the result. - Use
Isolate.runto return the list['Ali', 'Sara', 'Zayan']and print its length.
Application
-
Store
var name = 'Sara';, pass it into an isolate, and return it in upper case. -
What does this print?
var scores = [10, 20]; await Isolate.run(() => scores.clear()); print(scores); -
Use
Isolate.runto throwException('Disk full')and catch it inmain, printingError: Exception: Disk full. -
Return a record
(min, max)from an isolate for the list[31, 18, 44, 27].
Challenge
- Sum the numbers from 1 to 200,000,000 in an isolate and time it with
Stopwatch. - Write a top-level
int visits = 0;. Set it to5inside an isolate and return it. Print both the returned value andvisitsinmain. - Time
[1, 2, 3].lengthonmainand inIsolate.run(in microseconds). Which is faster, and why?
Practice Answers
Question 1 — Solution
import 'dart:isolate';
void main() {
print(Isolate.current.debugName);
}mainQuestion 2 — Solution
import 'dart:isolate';
void main() async {
print(await Isolate.run(() => 12 * 12));
}144Question 3 — Solution
import 'dart:isolate';
void main() async {
var names = await Isolate.run(() => ['Ali', 'Sara', 'Zayan']);
print(names.length);
}3Question 4 — Solution
import 'dart:isolate';
void main() async {
var name = 'Sara';
print(await Isolate.run(() => name.toUpperCase()));
}SARAQuestion 5 — Solution
[10, 20]The isolate cleared its own copy; scores in main is untouched.
Question 6 — Solution
import 'dart:isolate';
void main() async {
try {
await Isolate.run(() => throw Exception('Disk full'));
} catch (e) {
print('Error: $e');
}
}Error: Exception: Disk fullQuestion 7 — Solution
import 'dart:isolate';
void main() async {
var temps = [31, 18, 44, 27];
var (low, high) = await Isolate.run(() {
var min = temps[0];
var max = temps[0];
for (var t in temps) {
if (t < min) min = t;
if (t > max) max = t;
}
return (min, max);
});
print('Min: $low, max: $high');
}Min: 18, max: 44Question 8 — Solution
import 'dart:isolate';
void main() async {
var clock = Stopwatch()..start();
var sum = await Isolate.run(() {
var total = 0;
for (var i = 1; i <= 200000000; i++) {
total += i;
}
return total;
});
print('Sum: $sum in ${clock.elapsedMilliseconds} ms');
}Sum: 20000000100000000 in 156 msQuestion 9 — Solution
import 'dart:isolate';
int visits = 0;
void main() async {
var returned = await Isolate.run(() {
visits = 5;
return visits;
});
print('Returned: $returned, main: $visits');
}Returned: 5, main: 0Question 10 — Solution
import 'dart:isolate';
void main() async {
var list = [1, 2, 3];
var clock = Stopwatch()..start();
var a = list.length;
print('Main: $a in ${clock.elapsedMicroseconds} µs');
clock.reset();
var b = await Isolate.run(() => list.length);
print('Isolate: $b in ${clock.elapsedMicroseconds} µs');
}Main: 3 in 153 µs
Isolate: 3 in 5386 µsmain wins easily: the work is tiny, so starting an isolate and copying data costs far more
than the work itself.
Quick Revision
- What two things does every isolate have of its own?
- What is the name of the isolate every program starts in?
- What does
Isolate.runreturn? - Why do ticks stay on time while
Isolate.runworks? - How do you pass a value into
Isolate.run? - If an isolate changes a list it received, does
mainsee the change? - Name one kind of object that cannot be sent to an isolate.
- When is an isolate not worth it?
Key Takeaways
- An isolate has its own memory and its own event loop; isolates share no state.
- Every program starts in the
mainisolate. - Heavy calculation on
mainfreezes everything;asyncalone cannot fix it. Isolate.run(() => work())runs the work in a new isolate and returns aFuture.- Values used in the closure are copied in; the result is copied back.
- Changes inside the isolate — even to globals — never reach
main. Return results instead. - Errors thrown in the isolate fail the future, so
try/catchworks. - Objects tied to resources, like an open file handle, cannot be sent.
- Starting an isolate costs a few milliseconds — use it for long work only.
What's Next?
Lecture 099 — Working with Isolates. Isolate.run is a one-off: one job, one result,
then the isolate is gone. Next we will keep an isolate alive and talk to it again and again
using SendPort and ReceivePort, sending messages both ways.