Learn Dart - Asynchronous Programming
Series/Learn Dart/Episode 7
Episode 7 of 23

Learn Dart - Asynchronous Programming

This episode dissects asynchronous programming in Dart: Future with async and await, error handling, the basics of Stream with listeners and transformations, the Isolate model for concurrency, and asynchronous I/O use cases in real applications.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

Modern applications can't wait: reading files, making HTTP calls, or waiting for socket connections must run without freezing the interface. Dart solves this with an elegant asynchronous single-threaded model — you write code that looks synchronous, but runs without blocking.

Episode 7 dissects the three pillars of asynchronous programming in Dart: Future, Stream, and Isolate. You'll learn to write and combine async code with async and await, handle errors, transform data flows, and move heavy work to isolates.

After this episode, you'll be confident writing servers, network clients, and responsive applications.

Future, async, and await

Creating a Future

A Future represents a computation whose result becomes available later:

Future with async and await
Future<String> ambilData() async {
  await Future.delayed(Duration(seconds: 1));
  return 'data dari jaringan';
}
 
void main() async {
  var data = await ambilData();
  print(data);
}

await ambilData() suspends the execution of the main function without blocking the isolate. While waiting, the runtime can run other code — this is why a single thread can serve many I/O operations.

Handling Errors

Async still needs error handling. Use try and catch as usual:

Error handling on Future
Future<void> cekKoneksi() async {
  await Future.error(Exception('gagal koneksi'));
}
 
void main() async {
  try {
    await cekKoneksi();
  } catch (e) {
    print('Terjadi error: $e');
  }
}

await Future.error(...) throws an error caught by the catch block. This pattern is the same as synchronous error handling, so the mental transition is smooth.

Stream Basics and Transformation

StreamController and Listener

Unlike a Future, which produces a single value, a Stream produces many values over time:

StreamController and listen
import 'dart:async';
 
void main() {
  var controller = StreamController<int>();
 
  controller.stream.listen((data) {
    print('Terima: $data');
  });
 
  controller.add(1);
  controller.add(2);
  controller.close();
}

controller.stream.listen(...) registers a listener that's called every time a new value arrives. This pattern is used for events, progress bars, and real-time updates.

Transforming Streams

Streams can be transformed with methods like map, where, and take:

Transforming a stream
import 'dart:async';
 
void main() async {
  var stream = Stream.fromIterable([1, 2, 3, 4, 5]);
  var genap = stream.where((n) => n.isEven).map((n) => n * 10);
 
  await for (var nilai in genap) {
    print(nilai);
  }
}

await for (var nilai in genap) consumes stream values sequentially. The combination of where and map builds a declarative data transformation pipeline.

The Isolate Model for Concurrency

Isolate: Thread with Separate Memory

For CPU-heavy work like large parses or cryptography, don't block the main isolate. Move it to another isolate — an independent execution unit that shares messages, not memory:

Run work in an isolate
import 'dart:isolate';
 
void hitung(int data) {
  var total = 0;
  for (var i = 1; i <= data; i++) {
    total += i;
  }
  print('Hasil: $total');
}
 
void main() async {
  await Isolate.spawn(hitung, 1000000);
  print('Isolate utama tetap responsif');
}

Isolate.spawn(hitung, 1000000) runs the hitung function in a new isolate. Since isolates don't share memory, there's no race condition — communication happens only through ports and messages.

Asynchronous I/O and Event-Driven Use Cases

Asynchronous I/O is the backbone of server and client applications. For example, reading a file without blocking:

Read a file asynchronously
import 'dart:io';
 
Future<void> main() async {
  var file = File('catatan.txt');
  var isi = await file.readAsString();
  print(isi);
}

file.readAsString() returns a Future that completes when the entire content is read. For large files, use file.openRead(), which returns a per-chunk Stream. The same pattern applies to HTTP, databases, and sockets.

Tip

If you often write await inside a sequential loop, consider Future.wait to run independent tasks in parallel and save time.

Conclusion

Key takeaways:

  • Future represents a single value in the future; async and await make it readable.
  • Async errors are handled with try and catch just like synchronous code.
  • Stream produces many values over time; listen to consume, where and map to transform.
  • await for consumes stream values sequentially.
  • Isolate executes heavy work without sharing memory with the main isolate.
  • readAsString, HTTP, and databases all follow the same async pattern.

In the next episode 8, we'll cover package and dependency management — writing Dart packages, structuring pubspec.yaml, the difference between public and local packages, the pub.dev ecosystem with versioning, and using dart pub get and dart pub upgrade.