Watch the video (10 min)
On this page23 sections
  1. Prerequisites
  2. Connection With Previous Lectures
  3. Learning Objectives
  4. 1. What Is an Isolate?
  5. 2. You Are Already in an Isolate
  6. 3. The Problem: Heavy Work Blocks main
  7. 4. Isolate.run: Move the Work Out
  8. 5. Passing Data In, Getting a Result Out
  9. 6. Values Are Copied
  10. 7. No Shared State, Not Even Globals
  11. 8. Errors Come Back to main
  12. 9. What Cannot Be Sent
  13. 10. Cost vs Overhead
  14. 11. A Real-World Example
  15. Code Examples
  16. Common Beginner Mistakes
  17. Best Practices
  18. Mini Challenge
  19. Practice Questions
  20. Practice Answers
  21. Quick Revision
  22. Key Takeaways
  23. What's Next?

Lecture 98 — Isolates

Category16 — Concurrency & Isolates
Lecture098 of 140
Estimated video length10 min
TargetDart 3.12.2
LevelIntermediate

A heavy calculation freezes the whole program, because async only helps with waiting. An isolate runs the calculation somewhere else, so main keeps 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 ms or done at 811 ms will 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:

  • Stopwatch and blocking code (Lecture 087)
  • Future, async, and await (Lectures 088–089)
  • try/catch around await (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 main isolate
  • Run heavy work in a new isolate with Isolate.run
  • Prove that main stays 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:

dart
import 'dart:isolate';

void main() {
  print('Running in: ${Isolate.current.debugName}');
}
output
Running in: main

Everything 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:

dart
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:

output
348513 primes, done at 805 ms
tick at 807 ms
tick at 1001 ms

Ticks 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:

dart
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:

output
tick at 203 ms
tick at 402 ms
tick at 602 ms
tick at 802 ms
348513 primes, done at 811 ms

Now 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:

dart
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');
}
output
fib(40) = 102334155 in 757 ms

n 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:

dart
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');
}
output
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:

dart
import 'dart:isolate';

int counter = 0;

void main() async {
  await Isolate.run(() {
    counter = 99;
    print('Isolate sees counter = $counter');
  });
  print('Main sees counter = $counter');
}
output
Isolate sees counter = 99
Main sees counter = 0

To 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:

dart
import 'dart:isolate';

void main() async {
  try {
    await Isolate.run(() => throw FormatException('Bad mark: abc'));
  } on FormatException catch (e) {
    print('Caught in main: $e');
  }
}
output
Caught in main: FormatException: Bad mark: abc

9. 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):

dart
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();
}
output
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:

dart
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');
}
output
Main thread: 2000 in 222 µs
Isolate.run: 2000 in 4936 µs

About 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:

dart
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:

output
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 ms

Data in by closure, a record back, errors caught — and the screen never froze.


Code Examples

Example 1 — A First Isolate.run

dart
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');
}
output
Sum: 5000000050000000

Example 2 — Naming the Isolate

dart
import 'dart:isolate';

void main() async {
  var name = await Isolate.run(
    () => Isolate.current.debugName,
    debugName: 'marks-worker',
  );
  print('Main: ${Isolate.current.debugName}, worker: $name');
}
output
Main: main, worker: marks-worker

Example 3 — Passing Two Values In

dart
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');
}
output
7142857 multiples of 7

Example 4 — Returning a List

dart
import 'dart:isolate';

void main() async {
  var squares = await Isolate.run(() => [for (var i = 1; i <= 5; i++) i * i]);
  print(squares);
}
output
[1, 4, 9, 16, 25]

Example 5 — A Map Is Copied

dart
import 'dart:isolate';

void main() async {
  var stock = {'Pen': 10};
  await Isolate.run(() => stock['Pen'] = 0);
  print(stock);
}
output
{Pen: 10}

Example 6 — An Error Crosses Back

dart
import 'dart:isolate';

void main() async {
  try {
    await Isolate.run(() => throw StateError('Worker failed'));
  } catch (e) {
    print('Caught: $e');
  }
}
output
Caught: Bad state: Worker failed

Example 7 — Student Marks

dart
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');
}
output
5049500 of 10000000 students passed

Example 8 — Shopping Total

dart
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');
}
output
Total sales: 10989930000

Example 9 — Bank Balance Stays Safe

dart
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]}');
}
output
Projected: 4650, real: 1000

Example 10 — Hottest Temperature

dart
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');
}
output
Hottest: 112.8°F

Common Beginner Mistakes

1. Expecting a change inside the isolate to reach main

dart
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

dart
import 'dart:isolate';

void main() {
  var result = Isolate.run(() => 6 * 7);
  print(result);
}
output
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 main for quick work and waiting; move long calculations to Isolate.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(...) in try/catch, like any future.
  • In Flutter, the compute function does the same job as Isolate.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
dart
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:

output
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 ms

The ticks keep their 300 ms rhythm because the counting runs in another isolate.


Practice Questions

Solve each one before looking at the answers.

Beginner

  1. Print the name of the isolate main runs in.
  2. Use Isolate.run to calculate 12 * 12 and print the result.
  3. Use Isolate.run to return the list ['Ali', 'Sara', 'Zayan'] and print its length.

Application

  1. Store var name = 'Sara';, pass it into an isolate, and return it in upper case.

  2. What does this print?

    dart
    var scores = [10, 20];
    await Isolate.run(() => scores.clear());
    print(scores);
  3. Use Isolate.run to throw Exception('Disk full') and catch it in main, printing Error: Exception: Disk full.

  4. Return a record (min, max) from an isolate for the list [31, 18, 44, 27].

Challenge

  1. Sum the numbers from 1 to 200,000,000 in an isolate and time it with Stopwatch.
  2. Write a top-level int visits = 0;. Set it to 5 inside an isolate and return it. Print both the returned value and visits in main.
  3. Time [1, 2, 3].length on main and in Isolate.run (in microseconds). Which is faster, and why?

Practice Answers

Question 1 — Solution

dart
import 'dart:isolate';

void main() {
  print(Isolate.current.debugName);
}
output
main

Question 2 — Solution

dart
import 'dart:isolate';

void main() async {
  print(await Isolate.run(() => 12 * 12));
}
output
144

Question 3 — Solution

dart
import 'dart:isolate';

void main() async {
  var names = await Isolate.run(() => ['Ali', 'Sara', 'Zayan']);
  print(names.length);
}
output
3

Question 4 — Solution

dart
import 'dart:isolate';

void main() async {
  var name = 'Sara';
  print(await Isolate.run(() => name.toUpperCase()));
}
output
SARA

Question 5 — Solution

output
[10, 20]

The isolate cleared its own copy; scores in main is untouched.

Question 6 — Solution

dart
import 'dart:isolate';

void main() async {
  try {
    await Isolate.run(() => throw Exception('Disk full'));
  } catch (e) {
    print('Error: $e');
  }
}
output
Error: Exception: Disk full

Question 7 — Solution

dart
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');
}
output
Min: 18, max: 44

Question 8 — Solution

dart
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');
}
output
Sum: 20000000100000000 in 156 ms

Question 9 — Solution

dart
import 'dart:isolate';

int visits = 0;

void main() async {
  var returned = await Isolate.run(() {
    visits = 5;
    return visits;
  });
  print('Returned: $returned, main: $visits');
}
output
Returned: 5, main: 0

Question 10 — Solution

dart
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');
}
output
Main: 3 in 153 µs
Isolate: 3 in 5386 µs

main wins easily: the work is tiny, so starting an isolate and copying data costs far more than the work itself.


Quick Revision

  1. What two things does every isolate have of its own?
  2. What is the name of the isolate every program starts in?
  3. What does Isolate.run return?
  4. Why do ticks stay on time while Isolate.run works?
  5. How do you pass a value into Isolate.run?
  6. If an isolate changes a list it received, does main see the change?
  7. Name one kind of object that cannot be sent to an isolate.
  8. 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 main isolate.
  • Heavy calculation on main freezes everything; async alone cannot fix it.
  • Isolate.run(() => work()) runs the work in a new isolate and returns a Future.
  • 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/catch works.
  • 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.


Video lecture

Subscribe
Placeholder video — the real recording isn't linked yet.