async dan await

async dan await untuk pengurutan kode asinkron secara terstruktur.

Daftar isi

Rantai then pada topik sebelumnya bekerja dengan baik untuk satu atau dua langkah, tetapi rantai yang panjang membuat urutan logika sulit dibaca. Dengan kata kunci async dan await, kode asinkron ditulis dengan bentuk yang mirip kode sinkron, yaitu setiap langkah yang menunggu ditandai await, sedangkan sisanya tetap berurut seperti biasa. Topik ini membahas fungsi async dan nilai kembaliannya, urutan eksekusi di sekitar await, penggunaan await di dalam perulangan dibandingkan Future.wait untuk pekerjaan paralel, serta akibat adanya Future yang tidak ditunggu. Seluruh contoh tetap berjalan di terminal dengan dart:async bawaan.

Fungsi async dan await#

Fungsi yang diberi kata kunci async di awal tubuhnya selalu mengembalikan Future, bahkan ketika pernyataan return-nya menuliskan nilai biasa. Di dalam fungsi tersebut, await menangguhkan eksekusi sisa fungsi sampai Future yang diikutinya selesai, lalu melanjutkan dengan nilai hasilnya. Baris sebelum await dijalankan seketika, baris sesudahnya menunggu, dan seluruh urutan itu terbaca dari atas ke bawah tanpa callback. Tanpa kata kunci tersebut, penulisan await di dalam fungsi menjadi error saat kompilasi. Fungsi async yang dinyatakan void memiliki tipe kembalian Future<void>, sehingga pemanggilnya tetap dapat menandai selesainya pekerjaan melalui Future tersebut.

dart pemuatan.dart
Future<String> muatPengaturan() {
  return Future.delayed(const Duration(milliseconds: 50), () => "tema: gelap");
}

Future<String> muatCatatan() {
  return Future.delayed(const Duration(milliseconds: 50), () => "3 catatan");
}

Future<void> tampilkanPapan() async {
  final pengaturan = await muatPengaturan();
  final catatan = await muatCatatan();
  print("$pengaturan, $catatan");
}

void main() {
  print("membuka aplikasi catatan");
  tampilkanPapan().then((_) {
    print("papan siap ditampilkan");
  });
  print("persiapan selesai");
}
bash
# menjalankan program dari folder topik
dart run pemuatan.dart
text Keluaran
membuka aplikasi catatan
persiapan selesai
tema: gelap, 3 catatan
papan siap ditampilkan

Fungsi tampilkanPapan berisi dua await berturut-turut, dan keluarannya menunjukkan bahwa keduanya benar-benar berurutan, yaitu pengaturan tiba lebih dahulu sebelum catatan. Pada saat yang sama, pemanggilnya tidak tertahan, karena persiapan selesai dicetak sebelum kedua muatan tiba. Pemanggilan tampilkanPapan() menghasilkan Future<void> yang dilanjutkan dengan then, sehingga pesan papan siap ditampilkan dicetak setelah seluruh isi fungsi selesai. Fungsi main sendiri belum diberi kata kunci async karena tidak menggunakan await langsung, dan memberinya async pun sah apabila diperlukan.

Urutan eksekusi di sekitar await#

Pernyataan await membagi tubuh fungsi menjadi dua bagian yang dijalankan pada waktu berbeda. Bagian sebelum await berjalan seketika pada saat fungsi dipanggil, sedangkan bagian sesudahnya menjadi kelanjutan yang dijadwalkan setelah Future selesai. Pemahaman urutan ini penting karena pemanggil tidak pernah tertahan oleh await, melainkan hanya fungsi yang menunggunya. Akibat yang tampak di layar adalah log dari dua alur yang berselang-seling, padahal keduanya berasal dari satu fungsi yang terbaca linear.

dart urutan.dart
Future<String> isiBerkas(String nama) {
  return Future.delayed(
    const Duration(milliseconds: 50),
    () => "isi dari $nama",
  );
}

Future<void> bacaBerkas(String nama) async {
  print("meminta $nama");
  final isi = await isiBerkas(nama);
  print("menerima $nama: $isi");
}

void main() {
  print("mulai");
  bacaBerkas("catatan.txt");
  print("alur utama berlanjut");
}
bash
# menjalankan program dari folder topik
dart run urutan.dart
text Keluaran
mulai
meminta catatan.txt
alur utama berlanjut
menerima catatan.txt: isi dari catatan.txt

Keluaran memperlihatkan pembagian tubuh fungsi bacaBerkas dengan tepat. Pesan meminta catatan.txt berasal dari bagian sebelum await, yang berjalan seketika ketika fungsi dipanggil, sehingga muncul sebelum alur utama berlanjut dari main. Pesan menerima catatan.txt berasal dari kelanjutan setelah await, yang baru dijalankan setelah jeda 50 milidetik habis dan main sudah melanjutkan pekerjaannya. Selama jeda tersebut runtime tidak berhenti, karena antrean event terus melayani pekerjaan lain yang tersedia. Urutan semacam ini bersifat pasti dan bukan efek samping kecepatan mesin, karena kelanjutan await memang dijadwalkan pada antrean event.

Await dalam perulangan versus Future.wait#

await di dalam perulangan menjalankan tugas satu per satu, yaitu tugas berikutnya baru dimulai setelah tugas sebelumnya selesai. Bentuk ini tepat apabila setiap tugas bergantung pada hasil sebelumnya, tetapi boros waktu apabila tugas-tugas itu saling lepas. Untuk kasus yang saling lepas, daftar Future dibuat terlebih dahulu sehingga semuanya berjalan bersamaan, lalu Future.wait mengumpulkan hasilnya dalam satu await. Contoh berikut menjalankan kedua bentuk dengan tugas yang sama, lengkap dengan catatan waktunya di dalam fungsi pengukuran.

dart berurutan-paralel.dart
Future<int> pengukuran(int kelipatan, int nilai) {
  return Future.delayed(Duration(milliseconds: kelipatan * 100), () {
    print("pengukuran $nilai tiba");
    return nilai;
  });
}

Future<void> bacaBerurutan() async {
  final durasi = [3, 2, 1];
  final nilai = [30, 20, 10];
  for (var i = 0; i < durasi.length; i++) {
    final hasil = await pengukuran(durasi[i], nilai[i]);
    print("berurutan: $hasil");
  }
}

Future<void> bacaParalel() async {
  final durasi = [3, 2, 1];
  final nilai = [30, 20, 10];
  final hasil = await Future.wait([
    for (var i = 0; i < durasi.length; i++) pengukuran(durasi[i], nilai[i]),
  ]);
  print("paralel: $hasil");
}

void main() {
  print("versi berurutan dimulai");
  bacaBerurutan().then((_) {
    print("versi paralel dimulai");
    return bacaParalel();
  }).then((_) {
    print("kedua versi selesai");
  });
}
bash
# menjalankan program dari folder topik
dart run berurutan-paralel.dart
text Keluaran
versi berurutan dimulai
pengukuran 30 tiba
berurutan: 30
pengukuran 20 tiba
berurutan: 20
pengukuran 10 tiba
berurutan: 10
versi paralel dimulai
pengukuran 10 tiba
pengukuran 20 tiba
pengukuran 30 tiba
paralel: [30, 20, 10]
kedua versi selesai

Pada versi berurutan, pesan pengukuran 30 tiba muncul sebelum pesan pengukuran 20 tiba, karena tugas kedua baru dibuat setelah tugas pertama selesai, dan total waktunya menjumlahkan seluruh jeda. Pada versi paralel, tiga tugas dibuat sekaligus dengan collection-for di dalam literal daftarnya, sehingga pengukuran dengan jeda terpendek justru tiba lebih dahulu dan total waktunya hanya sepanjang jeda terlama. Hasil akhirnya tetap [30, 20, 10] sesuai urutan daftar, sebagaimana dijelaskan pada pembahasan Future.wait. Pemilihan bentuk dengan demikian bergantung pada pertanyaan yang sama, yaitu apakah tugas berikutnya membutuhkan hasil tugas sebelumnya atau tidak.

Future yang tidak ditunggu#

Pemanggilan fungsi async tanpa await tetap sah dan sering terjadi tanpa disengaja. Future yang dihasilkan berjalan sendiri, sehingga kode setelahnya tidak tertahan, namun tanggung jawab atas hasil dan errornya juga lepas. Apabila Future yang dibiarkan itu berakhir dengan error, error itu tidak dapat ditangkap oleh pemanggilnya dan muncul sebagai unhandled exception pada saat program berjalan. Analyzer Dart menyediakan aturan lint unawaited_futures untuk membantu menemukan pemanggilan yang lupa ditunggu, dan fungsi unawaited pada dart:async menandai pemanggilan yang memang sengaja dibiarkan.

dart unawaited.dart
Future<void> laporanBelakangan() async {
  await Future.delayed(const Duration(milliseconds: 50));
  print("laporan tiba belakangan");
}

void main() {
  laporanBelakangan();
  print("main selesai");
}
bash
# menjalankan program dari folder topik
dart run unawaited.dart
text Keluaran
main selesai
laporan tiba belakangan

Keluaran menunjukkan dua kejadian yang berurutan menurut waktu, bukan menurut urutan penulisannya. Pemanggilan laporanBelakangan() hanya memulai pekerjaan, sehingga main selesai dicetak lebih dahulu, dan pesan laporan baru muncul setelah jeda selesai. Program tetap menunggu sampai pekerjaan itu tuntas karena runtime menyelesaikan seluruh event yang masih tertunda sebelum berakhir, tetapi nilai kembaliannya tidak pernah diperiksa oleh siapa pun. Apabila laporanBelakangan berakhir dengan error, tidak ada penanganan yang menerima laporan tersebut, sehingga Future yang dibiarkan sebaiknya hanya untuk pekerjaan yang kegagalannya boleh terlewat.

Ringkasan#

  • Fungsi async selalu mengembalikan Future, dan await menangguhkan sisa tubuh fungsi sampai Future tersebut selesai.
  • Bagian sebelum await berjalan seketika saat fungsi dipanggil, sedangkan kelanjutannya dijadwalkan pada antrean event.
  • await di dalam perulangan menjalankan tugas berurutan, sedangkan daftar Future dengan Future.wait menjalankannya bersamaan.
  • Future yang tidak ditunggu melepas penanganan errornya, sehingga pemanggilan yang sengaja dibiarkan harus ditandai dengan unawaited.

Referensi#