kodingup.camp

Beranda/JavaScript Mastery: Prototype, Class, Waktu, Error & Async

Alur Eksekusi Asinkron

Modul 13 dari 154 menit bacaAsynchronous Execution Flow

Isi modul

    Kenapa ini penting

    Kamu sudah memakai setTimeout dan Promise. Tetapi pernahkah kamu bertanya: jika JavaScript hanya bisa mengerjakan satu hal dalam satu waktu, bagaimana ia bisa "menunggu" timer sambil tetap menjalankan kode lain? Dan kenapa Promise.resolve().then(...) selalu jalan sebelum setTimeout(..., 0) padahal keduanya "secepatnya"? Jawabannya adalah event loop, mesin penjadwal di jantung JavaScript.

    Memahami event loop membuat kamu bisa memprediksi urutan output kode asinkron, mengerti kenapa halaman membeku saat ada loop berat, dan menulis async/await dengan benar. async/await adalah cara modern menulis Promise yang terlihat seperti kode biasa dari atas ke bawah. Ini yang akan kamu pakai sehari-hari, jadi kamu perlu tahu persis kapan kode "berhenti menunggu" dan kapan tidak, termasuk cara menangani error-nya.

    Konsep inti

    Bagian-bagian mesin

    Analogi: sebuah restoran dengan satu koki (thread JavaScript).

    Komponen Analogi Peran
    Call stack Meja kerja koki Function yang sedang dikerjakan, tumpuk-menumpuk (terakhir masuk, pertama keluar)
    Web API / Node API Oven, kompor, kurir Timer, fetch, file, event klik dikerjakan di luar thread utama
    Task queue (macrotask) Antrean pesanan biasa Callback setTimeout, setInterval, event, I/O
    Microtask queue Antrean prioritas VIP Callback .then/.catch/.finally, lanjutan setelah await, queueMicrotask
    Event loop Manajer restoran Mengatur apa yang dikerjakan koki berikutnya

    Aturan event loop

    1. Jalankan semua kode sinkron sampai call stack kosong.
    2. Kosongkan seluruh microtask queue (termasuk microtask baru yang muncul selama proses ini).
    3. Ambil satu task dari task queue, jalankan.
    4. Kembali ke langkah 2. (Di browser, render layar bisa terjadi di antara task.)

    Konsekuensinya:

    async dan await

    // Dua cara yang setara
    function ambil() { return getData().then((d) => olah(d)); }
    async function ambil() { const d = await getData(); return olah(d); }
    

    Error handling async

    Gaya Cara menangkap
    Promise chain .catch(fn)
    async/await try { await ... } catch (e) { ... }
    Memanggil function async dari luar fungsiAsync().catch(...) atau await di dalam try

    Penolakan yang tidak ditangani memicu event unhandledrejection (browser) atau menghentikan proses Node.js. Pastikan setiap rantai berakhir di tempat yang menangani error.

    Berurutan vs paralel

    await di dalam loop menjalankan pekerjaan satu per satu. Jika pekerjaannya tidak saling bergantung, mulai semuanya dulu lalu tunggu bersama (Promise.all, dibahas lengkap di modul berikutnya). Ini bisa memangkas waktu tunggu drastis.

    Contoh kode

    // 1. Tebak urutan output: sinkron → microtask → task
    console.log("1. sinkron awal");
    
    setTimeout(() => console.log("6. task (setTimeout 0)"), 0);
    
    Promise.resolve()
      .then(() => console.log("3. microtask then #1"))
      .then(() => console.log("5. microtask then #2"));
    
    queueMicrotask(() => console.log("4. microtask queueMicrotask"));
    
    (async () => {
      console.log("2. di dalam async (sebelum await, masih sinkron)");
      await null;
      console.log("4b. setelah await (microtask)");
    })();
    
    console.log("2b. sinkron akhir");
    // → 1. sinkron awal
    // → 2. di dalam async (sebelum await, masih sinkron)
    // → 2b. sinkron akhir
    // → 3. microtask then #1
    // → 4. microtask queueMicrotask
    // → 4b. setelah await (microtask)
    // → 5. microtask then #2
    // → 6. task (setTimeout 0)
    

    Microtask dijalankan sesuai urutan masuk antrean: then #1, queueMicrotask, dan lanjutan await sudah mengantre saat kode sinkron berjalan. then #2 baru masuk antrean setelah then #1 selesai, jadi ia paling akhir di antara microtask.

    // 2. async/await dengan simulasi API
    const tunggu = (ms) => new Promise((r) => setTimeout(r, ms));
    
    async function ambilKurs(mataUang) {
      await tunggu(100); // simulasi jaringan
      const KURS = { USD: 16250, SGD: 12480, JPY: 107 };
      if (!(mataUang in KURS)) throw new Error(`Kurs ${mataUang} tidak tersedia`);
      return KURS[mataUang];
    }
    
    async function konversi(jumlah, mataUang) {
      try {
        const kurs = await ambilKurs(mataUang); // berhenti di sini, tapi tidak memblokir program
        return `${jumlah} ${mataUang} = Rp${(jumlah * kurs).toLocaleString("id-ID")}`;
      } catch (err) {
        return `Gagal konversi: ${err.message}`;
      } finally {
        console.log(`(selesai memproses ${mataUang})`);
      }
    }
    
    // 3. Function async mengembalikan Promise
    const hasil = konversi(50, "USD");
    console.log(hasil instanceof Promise); // → true
    
    // 4. Berurutan vs paralel
    async function main() {
      console.log(await hasil);
      // → (selesai memproses USD)
      // → 50 USD = Rp812.500
      console.log(await konversi(1000, "EUR"));
      // → (selesai memproses EUR)
      // → Gagal konversi: Kurs EUR tidak tersedia
    
      console.time("berurutan");
      const usd = await ambilKurs("USD");
      const sgd = await ambilKurs("SGD");
      const jpy = await ambilKurs("JPY");
      console.timeEnd("berurutan"); // → berurutan: 303ms (±3 x 100 ms)
    
      console.time("paralel");
      const [u, s, j] = await Promise.all([ambilKurs("USD"), ambilKurs("SGD"), ambilKurs("JPY")]);
      console.timeEnd("paralel"); // → paralel: 101ms (±100 ms)
      console.log(usd === u && sgd === s && jpy === j); // → true
    
      // 5. Jebakan: forEach tidak menunggu async
      const daftar = ["USD", "SGD"];
      daftar.forEach(async (m) => console.log("forEach:", m, await ambilKurs(m)));
      console.log("Ini tercetak SEBELUM hasil forEach!");
      await tunggu(150);
      // → Ini tercetak SEBELUM hasil forEach!
      // → forEach: USD 16250
      // → forEach: SGD 12480
    
      // Yang benar untuk berurutan: for...of
      for (const m of daftar) {
        console.log("for...of:", m, await ambilKurs(m));
      }
      // → for...of: USD 16250
      // → for...of: SGD 12480
    }
    
    main().catch((err) => console.error("Error tak terduga:", err)); // tangkap di level teratas
    
    // 6. Loop berat memblokir semuanya (timer pun harus menunggu)
    setTimeout(() => {
      const mulai = Date.now();
      setTimeout(() => console.log(`Timer 0 ms baru jalan setelah ${Date.now() - mulai} ms`), 0);
      while (Date.now() - mulai < 300) { /* blokir thread 300 ms */ }
    }, 1000);
    // → Timer 0 ms baru jalan setelah 300 ms
    

    Contoh 6 adalah demonstrasi paling penting: timer 0 ms harus menunggu 300 ms karena koki (thread) sedang sibuk. Di browser, selama 300 ms itu tombol tidak bisa diklik dan animasi berhenti. Pekerjaan berat seharusnya dipecah kecil-kecil atau dipindah ke Web Worker.

    Kesalahan umum

    1. Lupa await. const kurs = ambilKurs("USD") menghasilkan Promise, bukan angka. Perkalian dengan Promise menghasilkan NaN.
    2. Memakai await di dalam forEach/map dan mengira loop menunggu. forEach tidak peduli Promise. Pakai for...of (berurutan) atau Promise.all(arr.map(...)) (paralel).
    3. await berurutan untuk pekerjaan yang independen. Waktunya menjadi jumlah semua tunggu. Mulai bersamaan dengan Promise.all.
    4. Tidak menangani error function async. Memanggil main() tanpa .catch atau try/catch berujung unhandled rejection.
    5. Mengira await menghentikan seluruh program. Ia hanya menjeda function async tempat ia berada; kode lain dan event tetap berjalan.

    Latihan

    1. Mudah, Tanpa menjalankan, tebak urutan output: setTimeout(() => console.log("A"));, Promise.resolve().then(() => console.log("B"));, console.log("C");. Lalu buktikan.
    2. Sedang, Ubah rantai login().then(u => ambilTransaksi(u.id)).then(t => hitungSaldo(t)).catch(...) menjadi function async dengan try/catch. Buat function simulasinya.
    3. Menantang, Tulis async function prosesBertahap(items, fn, ukuranBatch) yang memproses items dalam batch: tiap batch berjalan paralel (Promise.all), antar-batch berurutan. Kembalikan semua hasil sesuai urutan.
    Lihat solusi
    // 1
    setTimeout(() => console.log("A"));
    Promise.resolve().then(() => console.log("B"));
    console.log("C");
    // → C   (sinkron)
    // → B   (microtask)
    // → A   (task)
    
    // 2
    const jeda = (ms, v) => new Promise((r) => setTimeout(() => r(v), ms));
    const login = () => jeda(50, { id: 7 });
    const ambilTransaksi = (id) => jeda(50, [{ jumlah: 500000 }, { jumlah: -120000 }]);
    const hitungSaldo = (t) => t.reduce((s, x) => s + x.jumlah, 0);
    
    async function tampilkanSaldo() {
      try {
        const user = await login();
        const trx = await ambilTransaksi(user.id);
        console.log("Saldo:", hitungSaldo(trx)); // → Saldo: 380000
      } catch (err) {
        console.log("Gagal:", err.message);
      }
    }
    
    // 3
    async function prosesBertahap(items, fn, ukuranBatch) {
      const hasil = [];
      for (let i = 0; i < items.length; i += ukuranBatch) {
        const batch = items.slice(i, i + ukuranBatch);
        hasil.push(...(await Promise.all(batch.map(fn))));
      }
      return hasil;
    }
    
    (async () => {
      await jeda(10);
      await tampilkanSaldo();
      const kuadrat = await prosesBertahap([1, 2, 3, 4, 5], (n) => jeda(50, n * n), 2);
      console.log(kuadrat); // → [ 1, 4, 9, 16, 25 ]  (3 batch: ±150 ms)
    })();
    

    Ringkasan

    Kuis pemahaman

    Pilih satu jawaban per soal. Penjelasan muncul setelah kamu menjawab.