kodingup.camp

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

Koordinasi Async Lanjutan

Modul 14 dari 154 menit bacaAdvanced Async Coordination

Isi modul

    Kenapa ini penting

    Aplikasi nyata jarang hanya menjalankan satu operasi asinkron. Dashboard keuangan mungkin perlu mengambil saldo, daftar transaksi, dan kurs mata uang sekaligus. Halaman produk memuat detail, ulasan, dan rekomendasi. Kamu perlu tahu cara menjalankan banyak Promise bersamaan dan memilih strategi yang tepat: tunggu semua? lanjut meskipun sebagian gagal? ambil yang tercepat?

    Selain itu kamu akan memakai fetch, API standar untuk mengambil data dari server yang tersedia di browser dan di Node.js (versi 18 ke atas). Kamu juga belajar dua teknik ketahanan yang membedakan aplikasi amatir dan profesional: membatalkan request yang sudah tidak dibutuhkan atau terlalu lama (AbortController), dan mencoba ulang (retry) saat jaringan sedang tidak stabil, kondisi yang sangat umum di jaringan seluler Indonesia.

    Konsep inti

    Empat combinator Promise

    Method Selesai ketika Hasil Gagal ketika
    Promise.all Semua fulfilled Array value (urutan sesuai input) Satu saja rejected (langsung)
    Promise.allSettled Semua settled Array { status, value } / { status, reason } Tidak pernah reject
    Promise.race Yang pertama settled Value/reason yang pertama Jika yang pertama rejected
    Promise.any Yang pertama fulfilled Value pertama yang berhasil Semua rejected → AggregateError

    Analogi: all itu seperti menunggu semua teman datang sebelum makan (satu batal, acara batal). allSettled seperti absensi: tunggu sampai semua menjawab, hadir atau tidak. race seperti lomba lari: siapa pun yang pertama mencapai garis, termasuk yang jatuh. any seperti mencari tukang ojek: ambil yang pertama bersedia, yang menolak diabaikan.

    Catatan: Promise yang "kalah" di race/any tidak otomatis dibatalkan, ia tetap berjalan di latar. Untuk benar-benar membatalkan, pakai AbortController.

    fetch dengan async/await

    const res = await fetch(url, { method, headers, body, signal });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    const data = await res.json();
    

    Dua hal yang sering mengejutkan:

    1. fetch hanya reject untuk kegagalan jaringan (offline, DNS gagal, dibatalkan). Status HTTP 404 atau 500 tetap dianggap "berhasil" dan Promise-nya fulfilled! Kamu wajib mengecek res.ok (true untuk status 200-299).
    2. Ada dua await: satu untuk header response, satu lagi untuk membaca body (res.json(), res.text()).

    Untuk mengirim data JSON: method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data).

    AbortController

    const controller = new AbortController();
    fetch(url, { signal: controller.signal });
    controller.abort(); // membatalkan → fetch reject dengan error bernama "AbortError"
    

    Jalan pintas modern: AbortSignal.timeout(ms) membuat signal yang otomatis abort setelah ms (error bernama "TimeoutError"), dan AbortSignal.any([s1, s2]) menggabungkan beberapa signal. Keduanya tersedia di browser modern dan Node.js 20+.

    Kegunaan: membatalkan pencarian lama saat pengguna mengetik kata baru, membatasi waktu tunggu, menghentikan request saat pengguna pindah halaman.

    Retry sederhana dengan backoff

    Kegagalan sementara (jaringan putus sebentar, server sibuk 503) sering berhasil jika dicoba lagi. Prinsip retry yang sehat:

    Contoh kode

    const tunggu = (ms) => new Promise((r) => setTimeout(r, ms));
    const sukses = (ms, v) => tunggu(ms).then(() => v);
    const gagal = (ms, pesan) => tunggu(ms).then(() => { throw new Error(pesan); });
    
    async function demoCombinator() {
      // 1. Promise.all: semua harus berhasil
      const [saldo, kurs] = await Promise.all([sukses(100, 2500000), sukses(50, 16250)]);
      console.log("all:", saldo, kurs); // → all: 2500000 16250
    
      try {
        await Promise.all([sukses(100, "A"), gagal(50, "server transaksi down"), sukses(80, "C")]);
      } catch (e) {
        console.log("all gagal:", e.message); // → all gagal: server transaksi down
      }
    
      // 2. Promise.allSettled: lihat nasib masing-masing
      const hasil = await Promise.allSettled([
        sukses(50, "saldo OK"),
        gagal(30, "kurs timeout"),
        sukses(70, "transaksi OK"),
      ]);
      hasil.forEach((h, i) =>
        console.log(`allSettled[${i}]:`, h.status, h.status === "fulfilled" ? h.value : h.reason.message));
      // → allSettled[0]: fulfilled saldo OK
      // → allSettled[1]: rejected kurs timeout
      // → allSettled[2]: fulfilled transaksi OK
    
      // 3. Promise.race: yang pertama selesai, berhasil atau gagal
      const tercepat = await Promise.race([sukses(120, "server Jakarta"), sukses(60, "server Singapura")]);
      console.log("race:", tercepat); // → race: server Singapura
    
      // 4. Promise.any: yang pertama BERHASIL
      const cermin = await Promise.any([gagal(20, "mirror 1 mati"), sukses(90, "mirror 2"), sukses(60, "mirror 3")]);
      console.log("any:", cermin); // → any: mirror 3
    
      try {
        await Promise.any([gagal(10, "x"), gagal(20, "y")]);
      } catch (e) {
        console.log("any gagal:", e.constructor.name, e.errors.map((x) => x.message)); // → any gagal: AggregateError [ 'x', 'y' ]
      }
    }
    
    // 5. fetch dengan async/await + cek res.ok
    async function ambilJSON(url, opsi = {}) {
      const res = await fetch(url, opsi);
      if (!res.ok) {
        const err = new Error(`HTTP ${res.status} untuk ${url}`);
        err.status = res.status;
        throw err;
      }
      return res.json();
    }
    
    async function demoFetch() {
      try {
        const user = await ambilJSON("https://jsonplaceholder.typicode.com/users/1");
        console.log("fetch:", user.name, "-", user.address.city); // → fetch: Leanne Graham - Gwenborough
    
        await ambilJSON("https://jsonplaceholder.typicode.com/tidak-ada");
      } catch (e) {
        console.log("fetch error:", e.message); // → fetch error: HTTP 404 untuk https://jsonplaceholder.typicode.com/tidak-ada
      }
    
      // POST JSON
      const baru = await ambilJSON("https://jsonplaceholder.typicode.com/posts", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ title: "Catatan belanja", body: "Beras 5 kg", userId: 1 }),
      });
      console.log("POST id:", baru.id); // → POST id: 101
    }
    
    // 6. AbortController: batal manual & timeout
    async function demoAbort() {
      const controller = new AbortController();
      setTimeout(() => controller.abort(), 5); // batalkan hampir seketika
      try {
        await fetch("https://jsonplaceholder.typicode.com/photos", { signal: controller.signal });
      } catch (e) {
        console.log("abort:", e.name); // → abort: AbortError
      }
    
      try {
        await fetch("https://jsonplaceholder.typicode.com/photos", { signal: AbortSignal.timeout(1) });
      } catch (e) {
        console.log("timeout:", e.name); // → timeout: TimeoutError
      }
    }
    
    // 7. Retry dengan exponential backoff
    async function denganRetry(fn, { percobaan = 3, jedaAwal = 200 } = {}) {
      for (let ke = 1; ; ke++) {
        try {
          return await fn(ke);
        } catch (err) {
          const permanen = err.status >= 400 && err.status < 500 && err.status !== 429;
          if (permanen || ke >= percobaan) throw err;
          const jeda = jedaAwal * 2 ** (ke - 1);
          console.log(`  percobaan ${ke} gagal (${err.message}), coba lagi dalam ${jeda} ms`);
          await tunggu(jeda);
        }
      }
    }
    
    async function demoRetry() {
      // server simulasi: gagal 2 kali lalu berhasil
      let panggilan = 0;
      const serverLabil = async () => {
        panggilan++;
        await tunggu(20);
        if (panggilan < 3) {
          const e = new Error("503 Service Unavailable");
          e.status = 503;
          throw e;
        }
        return { kursUSD: 16250 };
      };
      const data = await denganRetry(serverLabil);
      console.log("retry berhasil:", data);
      // →   percobaan 1 gagal (503 Service Unavailable), coba lagi dalam 200 ms
      // →   percobaan 2 gagal (503 Service Unavailable), coba lagi dalam 400 ms
      // → retry berhasil: { kursUSD: 16250 }
    }
    
    (async () => {
      await demoCombinator();
      await demoFetch().catch((e) => console.log("Jaringan tidak tersedia:", e.message));
      await demoAbort();
      await demoRetry();
    })();
    

    Contoh 5 dan 6 butuh koneksi internet (memakai API latihan gratis JSONPlaceholder). Contoh lainnya berjalan offline. Perhatikan denganRetry tidak mengulang error 4xx, karena request yang salah tidak akan menjadi benar hanya dengan diulang.

    Kesalahan umum

    1. Mengira fetch reject untuk 404/500. Tidak. Selalu cek res.ok atau res.status.
    2. Memakai Promise.all saat kegagalan sebagian masih bisa diterima. Satu widget gagal membuat seluruh dashboard kosong. Pakai allSettled dan tampilkan bagian yang berhasil.
    3. Lupa await res.json(). res.json() mengembalikan Promise; tanpa await kamu memegang Promise, bukan data.
    4. Retry tanpa batas atau tanpa jeda. Bisa membanjiri server yang sedang kesusahan dan menguras baterai/kuota pengguna. Batasi dan gunakan backoff.
    5. Mengira race membatalkan Promise yang kalah. Request yang kalah tetap berjalan. Gabungkan dengan AbortController jika memang ingin menghentikannya.

    Latihan

    1. Mudah, Ambil users/1, users/2, dan users/3 dari JSONPlaceholder secara paralel dengan Promise.all, lalu cetak nama-namanya.
    2. Sedang, Tulis ambilDenganTimeout(url, ms) memakai AbortController + setTimeout (bukan AbortSignal.timeout) yang melempar Error("Waktu habis") jika request lebih lama dari ms. Jangan lupa membersihkan timer.
    3. Menantang, Tulis muatDashboard() yang memanggil tiga sumber simulasi (saldo, transaksi, kurs; salah satunya gagal) dengan allSettled, lalu mengembalikan object { saldo, transaksi, kurs, peringatan: [...] } dengan nilai null untuk sumber yang gagal.
    Lihat solusi
    // 1
    async function tigaUser() {
      const ids = [1, 2, 3];
      const users = await Promise.all(
        ids.map((id) => fetch(`https://jsonplaceholder.typicode.com/users/${id}`).then((r) => r.json())),
      );
      console.log(users.map((u) => u.name)); // → [ 'Leanne Graham', 'Ervin Howell', 'Clementine Bauch' ]
    }
    
    // 2
    async function ambilDenganTimeout(url, ms) {
      const controller = new AbortController();
      const timer = setTimeout(() => controller.abort(), ms);
      try {
        const res = await fetch(url, { signal: controller.signal });
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return await res.json();
      } catch (err) {
        if (err.name === "AbortError") throw new Error("Waktu habis");
        throw err;
      } finally {
        clearTimeout(timer); // bersihkan timer apa pun hasilnya
      }
    }
    
    // 3
    const jeda = (ms, v, gagal) => new Promise((res, rej) => setTimeout(() => (gagal ? rej(new Error(v)) : res(v)), ms));
    async function muatDashboard() {
      const sumber = ["saldo", "transaksi", "kurs"];
      const hasil = await Promise.allSettled([
        jeda(50, 2500000),
        jeda(80, [{ ket: "Kopi", jumlah: -25000 }]),
        jeda(30, "layanan kurs tidak merespons", true),
      ]);
      const out = { peringatan: [] };
      hasil.forEach((h, i) => {
        out[sumber[i]] = h.status === "fulfilled" ? h.value : null;
        if (h.status === "rejected") out.peringatan.push(`${sumber[i]}: ${h.reason.message}`);
      });
      return out;
    }
    
    (async () => {
      await tigaUser().catch((e) => console.log("offline:", e.message));
      await ambilDenganTimeout("https://jsonplaceholder.typicode.com/photos", 1)
        .catch((e) => console.log(e.message)); // → Waktu habis
      console.log(await muatDashboard());
      // → { peringatan: [ 'kurs: layanan kurs tidak merespons' ], saldo: 2500000, transaksi: [ { ket: 'Kopi', jumlah: -25000 } ], kurs: null }
    })();
    

    Ringkasan

    Kuis pemahaman

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