Bab 11 dari Eloquent JavaScript. Sebelumnya: 10 Modul. Berikutnya: 12 Proyek Bahasa Pemrograman.

Siapa yang bisa menunggu dengan tenang sambil lumpur mengendap? Siapa yang bisa tetap diam sampai saatnya bertindak?

— Laozi, Tao Te Ching

Ilustrasi dua gagak di dahan pohon

Bagian tengah komputer, bagian yang ngejalanin langkah-langkah individual yang nyusun program kita, disebut processor. Program yang udah kita lihat sejauh ini bakal terus bikin processor sibuk sampai mereka selesai kerjaannya. Kecepatan sesuatu kaya loop yang memanipulasi angka bisa dieksekusi hampir sepenuhnya tergantung kecepatan processor dan memori komputer.

Tapi banyak program berinteraksi sama hal-hal di luar processor. Misalnya, mereka mungkin berkomunikasi lewat jaringan komputer atau minta data dari hard disk, yang jauh lebih lambat daripada dapetnya dari memori.

Waktu hal kaya gitu lagi terjadi, sayang banget kalau ngebiarin processor nganggur, mungkin ada kerjaan lain yang bisa dia lakuin sementara itu. Sebagian, ini ditangani sistem operasimu, yang bakal ngeganti processor antar beberapa program yang jalan. Tapi itu gak ngebantu waktu kita mau satu program bisa maju sementara dia lagi nunggu request jaringan.

Asinkronisitas

Di model pemrograman sinkron, hal-hal terjadi satu per satu. Waktu kamu manggil fungsi yang ngelakuin aksi yang jalan lama, dia baru return waktu aksinya selesai dan dia bisa ngembaliin hasilnya. Ini ngehentiin programmu selama aksinya berlangsung.

Model asinkron ngebolehin beberapa hal terjadi di saat yang sama. Waktu kamu mulai aksi, programmu terus jalan. Waktu aksinya selesai, programnya diberi tahu dan dapet akses ke hasilnya (misalnya, data yang dibaca dari disk).

Kita bisa ngebandingin pemrograman sinkron dan asinkron pakai contoh kecil: program yang bikin dua request lewat jaringan lalu ngegabungin hasilnya.

Di environment sinkron, di mana fungsi request baru return setelah dia selesai kerjaannya, cara termudah ngelakuin tugas ini adalah bikin request satu setelah yang lain. Ini punya kelemahan bahwa request kedua baru bakal dimulai waktu yang pertama selesai. Total waktu yang dibutuhin bakal setidaknya jumlah dari dua waktu respons.

Solusi buat masalah ini, di sistem sinkron, adalah mulai thread kontrol tambahan. Thread adalah program lain yang jalan yang eksekusinya mungkin diselang-seling sama program lain sama sistem operasi, karena kebanyakan komputer modern berisi beberapa processor, beberapa thread bahkan bisa jalan di saat yang sama, di processor yang berbeda. Thread kedua bisa mulai request kedua, dan lalu kedua thread nunggu hasilnya balik, setelah itu mereka resinkronisasi buat ngegabungin hasilnya.

Di diagram berikut, garis tebal mewakili waktu yang dihabiskan program buat jalan normal, dan garis tipis mewakili waktu yang dihabiskan buat nunggu jaringan. Di model sinkron, waktu yang dibutuhin jaringan adalah bagian dari timeline buat thread kontrol tertentu. Di model asinkron, mulai aksi jaringan ngebolehin program terus jalan sementara komunikasi jaringannya terjadi di sampingnya, ngasih tahu programnya waktu dia selesai.

Diagram yang menunjukkan alur kontrol di program sinkron dan asinkron. Bagian pertama nunjukin program sinkron, di mana fase aktif dan menunggu program semuanya terjadi di satu garis berurutan. Bagian kedua nunjukin program sinkron multi-thread, dengan dua garis paralel, di mana bagian menunggu terjadi berdampingan, bikin program selesai lebih cepat. Bagian terakhir nunjukin program asinkron, di mana beberapa aksi asinkron bercabang dari program utama, yang di suatu titik berhenti, lalu lanjut waktu hal pertama yang dia tunggu selesai.

Cara lain buat menggambarkan perbedaannya adalah menunggu aksi selesai itu implisit di model sinkron, sedangkan eksplisit, di bawah kendali kita, di model asinkron.

Asinkronisitas itu pisau bermata dua. Dia bikin ngekspresiin program yang gak cocok dengan model kontrol garis lurus lebih gampang, tapi dia juga bisa bikin ngekspresiin program yang emang ngikutin garis lurus lebih canggung. Kita bakal lihat beberapa cara ngurangin kecanggungan ini nanti di bab.

Kedua platform pemrograman JavaScript yang menonjol, browser dan Node.js, bikin operasi yang mungkin makan waktu jadi asinkron, bukannya ngandelin thread. Karena pemrograman dengan thread terkenal susah (memahami apa yang dilakuin program jauh lebih susah waktu dia ngelakuin beberapa hal sekaligus), ini umumnya dianggap hal yang bagus.

Callback

Salah satu pendekatan ke pemrograman asinkron adalah bikin fungsi yang perlu nunggu sesuatu ngambil argumen ekstra, fungsi callback. Fungsi asinkronnya mulai sebuah proses, nyiapin supaya fungsi callback-nya dipanggil waktu prosesnya selesai, lalu return.

Sebagai contoh, fungsi setTimeout, tersedia di Node.js maupun di browser, nunggu sejumlah milidetik tertentu lalu manggil fungsi.

setTimeout(() => console.log("Tick"), 500);

Menunggu umumnya bukan kerjaan penting, tapi dia bisa sangat berguna waktu kamu perlu ngatur supaya sesuatu terjadi di waktu tertentu atau ngecek apakah suatu aksi makan waktu lebih lama dari yang diharapkan.

Contoh lain dari operasi asinkron umum adalah baca file dari penyimpanan perangkat. Bayangin kamu punya fungsi readTextFile yang baca isi file sebagai string dan ngasihnya ke fungsi callback.

readTextFile("shopping_list.txt", content => {
  console.log(`Shopping List:\n${content}`);
});
// → Shopping List:
// → Peanut butter
// → Bananas

Fungsi readTextFile bukan bagian dari JavaScript standar. Kita bakal lihat cara baca file di browser dan di Node.js di bab-bab selanjutnya.

Ngelakuin beberapa aksi asinkron berturut-turut pakai callback berarti kamu harus terus ngasih fungsi baru buat nanganin lanjutan perhitungan setelah aksinya. Fungsi asinkron yang ngebandingin dua file dan ngehasilin boolean yang nunjukin apakah isinya sama mungkin keliatan kaya gini:

function compareFiles(fileA, fileB, callback) {
  readTextFile(fileA, contentA => {
    readTextFile(fileB, contentB => {
      callback(contentA == contentB);
    });
  });
}

Gaya pemrograman ini bisa dikerjain, tapi level indentasinya naik dengan setiap aksi asinkron karena kamu berakhir di fungsi lain. Ngelakuin hal yang lebih rumit, kaya ngebungkus aksi asinkron dalam loop, bisa jadi canggung.

Dalam satu sisi, asinkronisitas itu menular. Fungsi apa pun yang manggil fungsi yang bekerja secara asinkron harus sendiri jadi asinkron, pakai callback atau mekanisme serupa buat ngirim hasilnya. Manggil callback agak lebih rumit dan rawan salah daripada cuma ngembaliin nilai, jadi butuh nyusun bagian besar programmu dengan cara itu gak bagus.

Promise

Cara yang sedikit berbeda buat ngebangun program asinkron adalah bikin fungsi asinkron ngembaliin objek yang mewakili hasil (masa depan)-nya bukannya ngoper-oper fungsi callback. Dengan cara ini, fungsi kaya gitu sebenernya ngembaliin sesuatu yang bermakna, dan bentuk programnya lebih mirip program sinkron.

Inilah gunanya class standar Promise. Promise itu tanda terima yang mewakili nilai yang mungkin belum tersedia. Dia nyediain method then yang ngebolehin kamu mendaftarkan fungsi yang harus dipanggil waktu aksi yang dia tunggu selesai. Waktu promise-nya resolved (diselesaikan), artinya nilainya jadi tersedia, fungsi kaya gitu (bisa ada beberapa) dipanggil dengan nilai hasilnya. Bisa aja manggil then di promise yang udah resolved, fungsimu tetap bakal dipanggil.

Cara termudah bikin promise adalah manggil Promise.resolve. Fungsi ini mastiin nilai yang kamu kasih dibungkus dalam promise. Kalau dia udah promise, dia cuma dikembaliin. Kalau gak, kamu dapet promise baru yang langsung resolve dengan nilaimu sebagai hasilnya.

let fifteen = Promise.resolve(15);
fifteen.then(value => console.log(`Got ${value}`));
// → Got 15

Buat bikin promise yang gak langsung resolve, kamu bisa pakai Promise sebagai constructor. Dia punya interface yang agak aneh: constructor-nya ngarepin fungsi sebagai argumennya, yang langsung dia panggil, ngasihnya fungsi yang bisa dia pakai buat me-resolve promise-nya.

Misalnya, ini cara kamu bisa bikin interface berbasis promise buat fungsi readTextFile:

function textFile(filename) {
  return new Promise(resolve => {
    readTextFile(filename, text => resolve(text));
  });
}
 
textFile("plans.txt").then(console.log);

Perhatiin gimana, berbeda dengan fungsi gaya callback, fungsi asinkron ini ngembaliin nilai yang bermakna, promise buat ngasih kamu isi file di suatu titik di masa depan.

Hal berguna soal method then adalah dia sendiri ngembaliin promise lain. Yang ini resolve ke nilai yang dikembaliin fungsi callback atau, kalau nilai yang dikembaliin itu promise, ke nilai yang di-resolve promise itu. Jadi, kamu bisa “ngerantai” beberapa pemanggilan then buat nyiapin urutan aksi asinkron.

Fungsi ini, yang baca file penuh nama file dan ngembaliin isi file acak di daftar itu, nunjukin jenis pipeline promise asinkron ini:

function randomFile(listFile) {
  return textFile(listFile)
    .then(content => content.trim().split("\n"))
    .then(ls => ls[Math.floor(Math.random() * ls.length)])
    .then(filename => textFile(filename));
}

Fungsinya ngembaliin hasil dari rantai pemanggilan then ini. Promise awalnya ngambil daftar file sebagai string. Pemanggilan then pertama mentransformasi string itu jadi array baris, ngehasilin promise baru. Pemanggilan then kedua milih baris acak dari itu, ngehasilin promise ketiga yang ngasih satu nama file. Pemanggilan then terakhir baca file ini, jadi hasil fungsinya secara keseluruhan adalah promise yang ngembaliin isi file acak.

Di kode ini, fungsi yang dipakai di dua pemanggilan then pertama ngembaliin nilai biasa yang bakal langsung dioper ke promise yang dikembaliin then waktu fungsinya return. Pemanggilan then terakhir ngembaliin promise (textFile(filename)), bikinnya jadi langkah asinkron beneran.

Bisa juga ngelakuin semua langkah ini di dalam satu callback then, karena cuma langkah terakhir yang beneran asinkron. Tapi jenis pembungkus then yang cuma ngelakuin transformasi data sinkron sering berguna, kaya waktu kamu mau ngembaliin promise yang ngehasilin versi olahan dari suatu hasil asinkron.

function jsonFile(filename) {
  return textFile(filename).then(JSON.parse);
}
 
jsonFile("package.json").then(console.log);

Umumnya, berguna buat ngebayangin promise sebagai perangkat yang bikin kode bisa ngabaikan pertanyaan kapan sebuah nilai bakal tiba. Nilai normal harus beneran ada sebelum kita bisa ngerujuknya. Nilai yang dijanjikan adalah nilai yang mungkin udah ada di sana atau mungkin muncul di suatu titik di masa depan. Perhitungan yang didefinisikan dalam bentuk promise, dengan ngerangkainya bareng pemanggilan then, dieksekusi secara asinkron seiring inputnya jadi tersedia.

Kegagalan

Perhitungan JavaScript biasa bisa gagal dengan ngelempar exception. Perhitungan asinkron sering butuh sesuatu kaya gitu. Request jaringan mungkin gagal, file mungkin gak ada, atau sebagian kode yang jadi bagian perhitungan asinkron mungkin ngelempar exception.

Salah satu masalah paling mendesak dengan gaya callback pemrograman asinkron adalah dia bikin sangat susah mastiin kegagalan dilaporin dengan benar ke callback.

Konvensi umum adalah pakai argumen pertama ke callback buat nunjukin bahwa aksinya gagal, dan yang kedua buat ngasih nilai yang dihasilin aksinya waktu dia berhasil.

someAsyncFunction((error, value) => {
  if (error) handleError(error);
  else processValue(value);
});

Fungsi callback kaya gitu harus selalu ngecek apakah mereka nerima exception dan mastiin masalah apa pun yang mereka bikin, termasuk exception yang dilempar fungsi yang mereka panggil, ditangkep dan dikasih ke fungsi yang tepat.

Promise bikin ini lebih gampang. Mereka bisa entah resolved (aksinya selesai dengan sukses) atau rejected (dia gagal). Handler resolve (yang didaftarkan dengan then) cuma dipanggil waktu aksinya sukses, dan rejection disebarin ke promise baru yang dikembaliin then. Waktu handler ngelempar exception, ini otomatis bikin promise yang dihasilin pemanggilan then-nya jadi rejected. Kalau elemen apa pun di rantai aksi asinkron gagal, hasil dari seluruh rantainya ditandai sebagai rejected, dan gak ada handler sukses yang dipanggil melewati titik di mana dia gagal.

Sama kaya me-resolve promise nyediain nilai, me-reject satu juga nyediain nilai, biasanya disebut reason (alasan) dari rejection-nya. Waktu exception di fungsi handler bikin rejection, nilai exception-nya dipakai sebagai alasannya. Sama juga, waktu handler ngembaliin promise yang rejected, rejection itu mengalir ke promise berikutnya. Ada fungsi Promise.reject yang bikin promise baru yang langsung rejected.

Buat secara eksplisit nanganin rejection kaya gitu, promise punya method catch yang mendaftarkan handler buat dipanggil waktu promise-nya rejected, mirip gimana handler then nanganin resolusi normal. Dia juga sangat mirip then dalam hal dia ngembaliin promise baru, yang resolve ke nilai promise aslinya waktu itu resolve normal dan ke hasil handler catch kalau sebaliknya. Kalau handler catch ngelempar error, promise barunya juga rejected.

Sebagai jalan pintas, then juga nerima handler rejection sebagai argumen kedua, jadi kamu bisa pasang kedua jenis handler dalam satu pemanggilan method: .then(acceptHandler, rejectHandler).

Fungsi yang dikasih ke constructor Promise nerima argumen kedua, bareng fungsi resolve, yang bisa dia pakai buat me-reject promise barunya.

Waktu fungsi readTextFile kita nemuin masalah, dia ngasih error-nya ke fungsi callback-nya sebagai argumen kedua. Pembungkus textFile kita harusnya ngecek argumen itu supaya kegagalan bikin promise yang dia kembaliin jadi rejected.

function textFile(filename) {
  return new Promise((resolve, reject) => {
    readTextFile(filename, (text, error) => {
      if (error) reject(error);
      else resolve(text);
    });
  });
}

Rantai nilai promise yang dibikin pemanggilan then dan catch jadi membentuk pipeline tempat nilai atau kegagalan asinkron bergerak. Karena rantai kaya gitu dibikin dengan mendaftarkan handler, setiap mata rantai punya handler sukses atau handler rejection (atau keduanya) yang terkait dengannya. Handler yang gak cocok dengan jenis hasil (sukses atau gagal) diabaikan. Handler yang cocok dipanggil, dan hasilnya nentuin jenis nilai apa yang datang berikutnya, sukses waktu mereka ngembaliin nilai non-promise, rejection waktu mereka ngelempar exception, dan hasil promise-nya waktu mereka ngembaliin promise.

new Promise((_, reject) => reject(new Error("Fail")))
  .then(value => console.log("Handler 1:", value))
  .catch(reason => {
    console.log("Caught failure " + reason);
    return "nothing";
  })
  .then(value => console.log("Handler 2:", value));
// → Caught failure Error: Fail
// → Handler 2: nothing

Fungsi handler then pertama gak dipanggil karena di titik pipeline itu promise-nya megang rejection. Handler catch nanganin rejection itu dan ngembaliin nilai, yang dikasih ke fungsi handler then kedua.

Sama kaya exception yang gak ditangkep ditangani environment, environment JavaScript bisa ngedeteksi waktu rejection promise gak ditangani dan bakal ngelaporinnya sebagai error.

Carla

Hari yang cerah di Berlin. Landasan pacu bandara tua yang udah dinonaktifkan penuh sama pesepeda dan pemain sepatu roda. Di rumput dekat kontainer sampah, sekawanan gagak berkerumun berisik, nyoba ngeyakinin serombongan turis buat ngasih roti lapis mereka.

Salah satu gagaknya menonjol, betina besar yang lusuh dengan beberapa bulu putih di sayap kanannya. Dia mancing orang dengan keahlian dan kepercayaan diri yang nunjukin dia udah ngelakuin ini lama. Waktu seorang pria tua terganggu sama tingkah gagak lain, dia dengan santai menukik, nyamber roti setengah dimakannya dari tangannya, dan melayang pergi.

Berbeda dengan sisa kelompoknya, yang keliatan senang ngabisin hari main-main di sini, gagak besar itu keliatan punya tujuan. Sambil bawa jarahannya, dia terbang lurus ke atap bangunan hangar, menghilang ke ventilasi udara.

Di dalam bangunan, kamu bisa denger suara ketukan aneh, lembut, tapi terus-menerus. Dia datang dari ruang sempit di bawah atap tangga yang belum selesai. Gagaknya duduk di sana, dikelilingi camilan curiannya, setengah lusin smartphone (beberapa di antaranya nyala), dan tumpukan kabel. Dia cepat ngetuk layar salah satu ponsel dengan paruhnya. Kata-kata muncul di situ. Kalau kamu gak tahu, kamu bakal ngira dia lagi ngetik.

Gagak ini dikenal rekan-rekannya sebagai “cāāw-krö”. Tapi karena suara itu kurang cocok buat pita suara manusia, kita bakal nyebut dia Carla.

Carla adalah gagak yang agak aneh. Di masa mudanya, dia terpesona sama bahasa manusia, nguping orang sampai dia paham betul apa yang mereka bilang. Belakangan di hidupnya, minatnya bergeser ke teknologi manusia, dan dia mulai nyuri ponsel buat mempelajarinya. Proyeknya saat ini adalah belajar memprogram. Teks yang dia ketik di lab tersembunyinya, sebenernya, adalah sepotong kode JavaScript asinkron.

Menerobos Masuk

Carla suka internet. Menyebalkannya, ponsel yang lagi dia kerjain hampir kehabisan data prabayar. Bangunannya punya jaringan nirkabel, tapi butuh kode buat aksesnya.

Untungnya, router nirkabel di bangunan itu udah 20 tahun dan keamanannya buruk. Setelah riset, Carla nemuin bahwa mekanisme autentikasi jaringannya punya cacat yang bisa dia pakai. Waktu gabung ke jaringan, perangkat harus ngirim kode enam digit yang benar. Access point-nya bakal balas dengan pesan sukses atau gagal tergantung apakah kode yang benar dikasih. Tapi, waktu ngirim kode parsial (misalnya, cuma tiga digit), responsnya berbeda tergantung apakah digit-digit itu adalah awal kode yang benar atau bukan. Ngirim angka yang salah langsung ngembaliin pesan gagal. Waktu ngirim yang benar, access point-nya nunggu digit lebih banyak.

Ini bikin mungkin buat sangat mempercepat nebak angkanya. Carla bisa nemuin digit pertama dengan nyoba setiap angka bergantian, sampai dia nemuin satu yang gak langsung ngembaliin kegagalan. Punya satu digit, dia bisa nemuin digit kedua dengan cara yang sama, dan seterusnya, sampai dia tahu seluruh passcode-nya.

Anggap Carla punya fungsi joinWifi. Dikasih nama jaringan dan passcode (sebagai string), fungsinya nyoba gabung ke jaringan, ngembaliin promise yang resolve kalau sukses dan reject kalau autentikasinya gagal. Hal pertama yang dia butuhin adalah cara ngebungkus promise supaya dia otomatis reject setelah makan waktu terlalu banyak, buat ngebolehin programnya cepat lanjut kalau access point-nya gak merespons.

function withTimeout(promise, time) {
  return new Promise((resolve, reject) => {
    promise.then(resolve, reject);
    setTimeout(() => reject("Timed out"), time);
  });
}

Ini memanfaatkan fakta bahwa promise cuma bisa di-resolve atau di-reject sekali. Kalau promise yang dikasih sebagai argumennya resolve atau reject duluan, hasil itu bakal jadi hasil promise yang dikembaliin withTimeout. Kalau, di sisi lain, setTimeout-nya nyala duluan, me-reject promise-nya, pemanggilan resolve atau reject lebih lanjut diabaikan.

Buat nemuin seluruh passcode, programnya perlu berulang kali nyari digit berikutnya dengan nyoba setiap digit. Kalau autentikasi sukses, kita tahu kita udah nemuin yang kita cari. Kalau dia langsung gagal, kita tahu digit itu salah dan harus nyoba digit berikutnya. Kalau request-nya timeout, kita udah nemuin digit benar lain dan harus lanjut dengan nambahin digit lain.

Karena kamu gak bisa nunggu promise di dalam loop for, Carla pakai fungsi rekursif buat ngejalanin proses ini. Di setiap pemanggilan, fungsi ini dapet kode seperti yang kita tahu sejauh ini, juga digit berikutnya buat dicoba. Tergantung apa yang terjadi, dia mungkin ngembaliin kode yang selesai atau manggil dirinya sendiri, buat entah mulai ngeretas posisi berikutnya di kode atau nyoba lagi dengan digit lain.

function crackPasscode(networkID) {
  function nextDigit(code, digit) {
    let newCode = code + digit;
    return withTimeout(joinWifi(networkID, newCode), 50)
      .then(() => newCode)
      .catch(failure => {
        if (failure == "Timed out") {
          return nextDigit(newCode, 0);
        } else if (digit < 9) {
          return nextDigit(code, digit + 1);
        } else {
          throw failure;
        }
      });
  }
  return nextDigit("", 0);
}

Access point-nya cenderung merespons request autentikasi buruk dalam sekitar 20 milidetik, jadi buat aman, fungsi ini nunggu 50 milidetik sebelum nge-timeout request.

crackPasscode("HANGAR 2").then(console.log);
// → 555555

Carla memiringkan kepalanya dan mendesah. Ini bakal lebih memuaskan kalau kodenya sedikit lebih susah ditebak.

Fungsi Async

Bahkan dengan promise, jenis kode asinkron ini menyebalkan buat ditulis. Promise sering perlu dirangkai dengan cara yang bertele-tele dan keliatan sembarangan. Buat bikin loop asinkron, Carla dipaksa ngenalin fungsi rekursif.

Yang sebenernya dilakuin fungsi peretasan itu sepenuhnya linier, dia selalu nunggu aksi sebelumnya selesai sebelum mulai yang berikutnya. Di model pemrograman sinkron, dia bakal lebih lugas buat diekspresikan.

Kabar baiknya adalah JavaScript ngebolehin kamu nulis kode pseudo-sinkron buat menggambarkan perhitungan asinkron. Fungsi async secara implisit ngembaliin promise dan bisa, di badannya, await promise lain dengan cara yang keliatan sinkron.

Kita bisa nulis ulang crackPasscode kaya gini:

async function crackPasscode(networkID) {
  for (let code = "";;) {
    for (let digit = 0;; digit++) {
      let newCode = code + digit;
      try {
        await withTimeout(joinWifi(networkID, newCode), 50);
        return newCode;
      } catch (failure) {
        if (failure == "Timed out") {
          code = newCode;
          break;
        } else if (digit == 9) {
          throw failure;
        }
      }
    }
  }
}

Versi ini lebih jelas nunjukin struktur loop ganda fungsinya (loop dalam nyoba digit 0 sampai 9 dan loop luar nambahin digit ke passcode-nya).

Fungsi async ditandai dengan kata async sebelum keyword function. Method juga bisa dibikin async dengan nulis async sebelum namanya. Waktu fungsi atau method kaya gitu dipanggil, dia ngembaliin promise. Begitu fungsinya ngembaliin sesuatu, promise itu di-resolve. Kalau badannya ngelempar exception, promise-nya di-reject.

Di dalam fungsi async, kata await bisa ditaruh di depan ekspresi buat nunggu promise resolve dan baru lalu lanjut eksekusi fungsinya. Kalau promise-nya reject, exception diangkat di titik await-nya.

Fungsi kaya gitu gak lagi jalan dari awal sampai selesai sekaligus kaya fungsi JavaScript biasa. Sebagai gantinya, dia bisa dibekuin di titik mana pun yang punya await dan bisa dilanjutin di waktu belakangan.

Buat kebanyakan kode asinkron, notasi ini lebih praktis daripada langsung pakai promise. Kamu emang masih perlu pemahaman soal promise, karena di banyak kasus kamu masih bakal berinteraksi dengan mereka langsung. Tapi waktu ngerangkai mereka, fungsi async umumnya lebih enak ditulis daripada rantai pemanggilan then.

Generator

Kemampuan fungsi buat dijeda dan lalu dilanjutin lagi ini gak eksklusif buat fungsi async. JavaScript juga punya fitur bernama fungsi generator. Ini serupa, tapi tanpa promise.

Waktu kamu mendefinisikan fungsi dengan function* (naruh tanda bintang setelah kata function), dia jadi generator. Waktu kamu manggil generator, dia ngembaliin iterator, yang udah kita lihat di Bab 6.

function* powers(n) {
  for (let current = n;; current *= n) {
    yield current;
  }
}
 
for (let power of powers(3)) {
  if (power > 50) break;
  console.log(power);
}
// → 3
// → 9
// → 27

Awalnya, waktu kamu manggil powers, fungsinya dibekuin di awalnya. Setiap kali kamu manggil next di iterator-nya, fungsinya jalan sampai dia nemuin ekspresi yield, yang menjeda-nya dan bikin nilai yang di-yield jadi nilai berikutnya yang dihasilin iterator. Waktu fungsinya return (yang di contoh gak pernah), iterator-nya selesai.

Nulis iterator sering jauh lebih gampang waktu kamu pakai fungsi generator. Iterator buat class Group (dari latihan di Bab 6) bisa ditulis dengan generator ini:

Group.prototype[Symbol.iterator] = function*() {
  for (let i = 0; i < this.members.length; i++) {
    yield this.members[i];
  }
};

Gak perlu lagi bikin objek buat megang state iterasi, generator otomatis nyimpen state lokal mereka setiap kali mereka yield.

Ekspresi yield kaya gitu cuma boleh muncul langsung di fungsi generatornya sendiri dan bukan di fungsi dalam yang kamu definisikan di dalamnya. State yang disimpan generator, waktu yield, cuma environment lokal-nya dan posisi tempat dia yield.

Fungsi async adalah jenis generator khusus. Dia ngehasilin promise waktu dipanggil, yang di-resolve waktu dia return (selesai) dan di-reject waktu dia ngelempar exception. Setiap kali dia yield (await) sebuah promise, hasil promise itu (nilai atau exception yang dilempar) adalah hasil dari ekspresi await.

Proyek Seni Gagak

Suatu pagi, Carla terbangun oleh suara asing dari landasan di luar hangarnya. Melompat ke pinggir atap, dia lihat manusia lagi nyiapin sesuatu. Ada banyak kabel listrik, panggung, dan semacam dinding hitam besar yang lagi dibangun.

Sebagai gagak yang penasaran, Carla ngelihat lebih dekat dindingnya. Dia keliatan terdiri dari sejumlah perangkat besar berlayar kaca yang dikabelin ke kabel. Di belakang, perangkatnya bertuliskan “LedTec SIG-5030”.

Pencarian internet cepat ngasih manual pengguna buat perangkat ini. Mereka keliatannya rambu lalu lintas, dengan matriks lampu LED amber yang bisa diprogram. Maksud manusianya mungkin buat nampilin semacam informasi di mereka selama acara mereka. Menariknya, layarnya bisa diprogram lewat jaringan nirkabel. Bisa jadi mereka terhubung ke jaringan lokal bangunan?

Setiap perangkat di jaringan dapet alamat IP, yang bisa dipakai perangkat lain buat ngirim pesan ke dia. Kita ngomongin itu lebih lanjut di Bab 13. Carla nyadar bahwa ponsel-ponselnya sendiri semua dapet alamat kaya 10.0.0.20 atau 10.0.0.33. Mungkin layak buat nyoba ngirim pesan ke semua alamat kaya gitu dan lihat apakah salah satunya merespons interface yang digambarkan di manual buat rambunya.

Bab 18 nunjukin cara bikin request beneran di jaringan beneran. Di bab ini, kita bakal pakai fungsi dummy yang disederhanakan bernama request buat komunikasi jaringan. Fungsi ini ngambil dua argumen, alamat jaringan dan pesan, yang bisa apa pun yang bisa dikirim sebagai JSON, dan ngembaliin promise yang entah resolve ke respons dari mesin di alamat yang dikasih, atau reject kalau ada masalah.

Menurut manualnya, kamu bisa ngubah apa yang ditampilin di rambu SIG-5030 dengan ngirim dia pesan dengan konten kaya {"command": "display", "data": [0, 0, 3, …]}, di mana data megang satu angka per titik LED, nyediain kecerahannya, 0 berarti mati, 3 berarti kecerahan maksimum. Setiap rambu lebarnya 50 lampu dan tingginya 30 lampu, jadi perintah update harus ngirim 1.500 angka.

Kode ini ngirim pesan update tampilan ke semua alamat di jaringan lokal, buat lihat mana yang nyangkut. Setiap angka di alamat IP bisa dari 0 sampai 255. Di data yang dia kirim, dia ngaktifin sejumlah lampu yang sesuai dengan angka terakhir alamat jaringannya.

for (let addr = 1; addr < 256; addr++) {
  let data = [];
  for (let n = 0; n < 1500; n++) {
    data.push(n < addr ? 3 : 0);
  }
  let ip = `10.0.0.${addr}`;
  request(ip, {command: "display", data})
    .then(() => console.log(`Request to ${ip} accepted`))
    .catch(() => {});
}

Karena kebanyakan alamat ini gak bakal ada atau gak bakal nerima pesan kaya gitu, pemanggilan catch mastiin error jaringan gak bikin programnya crash. Request-nya semua dikirim langsung, tanpa nunggu request lain selesai, biar gak buang waktu waktu beberapa mesin gak jawab.

Setelah nembakin scan jaringannya, Carla balik keluar buat lihat hasilnya. Dengan senang, semua layar sekarang nampilin garis cahaya di sudut kiri atas mereka. Mereka emang di jaringan lokal, dan mereka emang nerima perintah. Dia cepat nyatet angka yang ditampilin di setiap layar. Ada sembilan layar, disusun tiga tinggi dan tiga lebar. Mereka punya alamat jaringan berikut:

const screenAddresses = [
  "10.0.0.44", "10.0.0.45", "10.0.0.41",
  "10.0.0.31", "10.0.0.40", "10.0.0.42",
  "10.0.0.48", "10.0.0.47", "10.0.0.46"
];

Sekarang ini membuka kemungkinan buat segala macam kenakalan. Dia bisa nampilin “gagak berkuasa, manusia ngiler” di dinding dengan huruf raksasa. Tapi itu terasa agak kasar. Sebagai gantinya, dia berencana nampilin video gagak terbang yang menutupi semua layar di malam hari.

Carla nemuin klip video yang pas, di mana satu setengah detik rekaman bisa diulang buat bikin video loop yang nunjukin kepakan sayap gagak. Biar pas dengan sembilan layar (yang masing-masing bisa nampilin 50×30 piksel), Carla motong dan mengubah ukuran videonya buat dapet serangkaian gambar 150×90, 10 per detik. Itu lalu masing-masing dipotong jadi sembilan persegi panjang, dan diproses supaya titik gelap di video (di mana gagaknya) nampilin cahaya terang, dan titik terang (gak ada gagak) dibiarin gelap, yang harusnya bikin efek gagak amber yang terbang melawan latar hitam.

Dia udah nyiapin variabel clipImages buat megang array frame, di mana setiap frame diwakili dengan array dari sembilan set piksel, satu buat setiap layar, dalam format yang diharapkan rambunya.

Buat nampilin satu frame video, Carla perlu ngirim request ke semua layar sekaligus. Tapi dia juga perlu nunggu hasil request ini, baik biar gak mulai ngirim frame berikutnya sebelum yang sekarang udah terkirim dengan benar dan biar nyadar waktu request lagi gagal.

Promise punya method static all yang bisa dipakai buat ngonversi array promise jadi satu promise yang resolve ke array hasil. Ini nyediain cara praktis buat bikin beberapa aksi asinkron terjadi berdampingan, nunggu mereka semua selesai, lalu ngelakuin sesuatu dengan hasilnya (atau setidaknya nunggu mereka buat mastiin mereka gak gagal).

function displayFrame(frame) {
  return Promise.all(frame.map((data, i) => {
    return request(screenAddresses[i], {
      command: "display",
      data
    });
  }));
}

Ini me-map gambar di frame (yang array dari array data tampilan) buat bikin array promise request. Dia lalu ngembaliin promise yang ngegabungin semuanya.

Biar bisa ngehentiin video yang lagi main, prosesnya dibungkus dalam class. Class ini punya method play asinkron yang ngembaliin promise yang cuma resolve waktu pemutarannya dihentiin lagi lewat method stop.

function wait(time) {
  return new Promise(accept => setTimeout(accept, time));
}
 
class VideoPlayer {
  constructor(frames, frameTime) {
    this.frames = frames;
    this.frameTime = frameTime;
    this.stopped = true;
  }
 
  async play() {
    this.stopped = false;
    for (let i = 0; !this.stopped; i++) {
      let nextFrame = wait(this.frameTime);
      await displayFrame(this.frames[i % this.frames.length]);
      await nextFrame;
    }
  }
 
  stop() {
    this.stopped = true;
  }
}

Fungsi wait ngebungkus setTimeout dalam promise yang resolve setelah sejumlah milidetik yang dikasih. Ini berguna buat ngendaliin kecepatan pemutarannya.

let video = new VideoPlayer(clipImages, 100);
video.play().catch(e => {
  console.log("Playback failed: " + e);
});
setTimeout(() => video.stop(), 15000);

Selama seminggu penuh dinding layar itu berdiri, setiap malam, waktu gelap, burung oranye bercahaya raksasa misterius muncul di situ.

Event Loop

Program asinkron dimulai dengan ngejalanin script utamanya, yang sering bakal nyiapin callback buat dipanggil belakangan. Script utama itu, juga callback-nya, jalan sampai selesai dalam satu potongan, tanpa terputus. Tapi di antara mereka, programnya mungkin nganggur, nunggu sesuatu terjadi.

Jadi callback gak langsung dipanggil sama kode yang menjadwalkannya. Kalau saya manggil setTimeout dari dalam fungsi, fungsi itu bakal udah return waktu fungsi callback-nya dipanggil. Dan waktu callback-nya return, kontrol gak balik ke fungsi yang menjadwalkannya.

Perilaku asinkron terjadi di call stack fungsinya sendiri yang kosong. Ini salah satu alasan kenapa, tanpa promise, ngelola exception di seluruh kode asinkron itu begitu susah. Karena setiap callback mulai dengan stack yang kebanyakan kosong, handler catch-mu gak bakal ada di stack waktu mereka ngelempar exception.

try {
  setTimeout(() => {
    throw new Error("Woosh");
  }, 20);
} catch (e) {
  // Ini gak bakal jalan
  console.log("Caught", e);
}

Gak peduli seberapa dekat event, kaya timeout atau request masuk, terjadi, environment JavaScript bakal cuma ngejalanin satu program dalam satu waktu. Kamu bisa ngebayangin ini kaya dia ngejalanin loop besar di sekitar programmu, disebut event loop. Waktu gak ada yang perlu dilakuin, loop itu dijeda. Tapi seiring event masuk, mereka ditambahin ke antrean, dan kodenya dieksekusi satu setelah yang lain. Karena gak ada dua hal yang jalan di saat yang sama, kode yang jalan lambat bisa nunda penanganan event lain.

Contoh ini nyetel timeout tapi lalu berlama-lama sampai setelah titik waktu yang dimaksudkan timeout-nya, bikin timeout-nya terlambat.

let start = Date.now();
setTimeout(() => {
  console.log("Timeout ran at", Date.now() - start);
}, 20);
while (Date.now() < start + 50) {}
console.log("Wasted time until", Date.now() - start);
// → Wasted time until 50
// → Timeout ran at 55

Promise selalu resolve atau reject sebagai event baru. Bahkan kalau promise udah resolved, nunggunya bakal bikin callback-mu jalan setelah script saat ini selesai, bukannya langsung.

Promise.resolve("Done").then(console.log);
console.log("Me first!");
// → Me first!
// → Done

Di bab-bab selanjutnya kita bakal lihat berbagai jenis event lain yang jalan di event loop.

Bug Asinkron

Waktu programmu jalan secara sinkron, dalam satu kali jalan, gak ada perubahan state yang terjadi kecuali yang dibikin programnya sendiri. Buat program asinkron ini beda, mereka mungkin punya celah di eksekusi mereka di mana kode lain bisa jalan.

Kita lihat contoh. Ini fungsi yang nyoba ngelaporin ukuran setiap file di array file, mastiin baca mereka semua di saat yang sama bukannya berurutan.

async function fileSizes(files) {
  let list = "";
  await Promise.all(files.map(async fileName => {
    list += fileName + ": " +
      (await textFile(fileName)).length + "\n";
  }));
  return list;
}

Bagian async fileName => nunjukin gimana arrow function juga bisa dibikin async dengan naruh kata async di depannya.

Kodenya gak langsung keliatan mencurigakan… dia me-map arrow function async di array nama, bikin array promise, lalu pakai Promise.all buat nunggu semua ini sebelum ngembaliin daftar yang mereka bangun.

Tapi program ini sepenuhnya rusak. Dia bakal selalu ngembaliin cuma satu baris output, ngedaftarin file yang paling lama dibaca.

fileSizes(["plans.txt", "shopping_list.txt"])
  .then(console.log);

Bisa kamu cari tahu kenapa?

Masalahnya ada di operator +=, yang ngambil nilai list saat ini di waktu statement-nya mulai dieksekusi lalu, waktu await-nya selesai, nyetel binding list jadi nilai itu ditambah string yang ditambahin.

Tapi di antara waktu statement-nya mulai dieksekusi dan waktu dia selesai, ada celah asinkron. Ekspresi map-nya jalan sebelum apa pun ditambahin ke list, jadi setiap operator += mulai dari string kosong dan berakhir, waktu pengambilan penyimpanannya selesai, nyetel list jadi hasil dari nambahin barisnya ke string kosong.

Ini bisa dengan gampang dihindari dengan ngembaliin baris-baris dari promise yang di-map dan manggil join di hasil Promise.all, bukannya ngebangun daftarnya dengan ngubah binding. Kaya biasa, ngitung nilai baru lebih kecil kemungkinan salahnya daripada ngubah nilai yang ada.

async function fileSizes(files) {
  let lines = files.map(async fileName => {
    return fileName + ": " +
      (await textFile(fileName)).length;
  });
  return (await Promise.all(lines)).join("\n");
}

Kesalahan kaya ini gampang dibikin, terutama waktu pakai await, dan kamu sebaiknya sadar di mana celah di kodemu terjadi. Keuntungan dari asinkronisitas JavaScript yang eksplisit (baik lewat callback, promise, atau await) adalah nemuin celah ini relatif gampang.

Ringkasan

Pemrograman asinkron bikin mungkin buat ngekspresiin menunggu aksi yang jalan lama tanpa membekukan seluruh program. Environment JavaScript biasanya ngimplementasiin gaya pemrograman ini pakai callback, fungsi yang dipanggil waktu aksinya selesai. Event loop menjadwalkan callback kaya gitu buat dipanggil di saat yang tepat, satu setelah yang lain, biar eksekusi mereka gak tumpang tindih.

Pemrograman secara asinkron dibikin lebih gampang oleh promise, objek yang mewakili aksi yang mungkin selesai di masa depan, dan fungsi async, yang ngebolehin kamu nulis program asinkron seolah dia sinkron.

Poin-poin pentingnya:

  1. Sinkron = satu per satu, program berhenti nunggu. Asinkron = program lanjut jalan sambil aksinya berlangsung, lalu diberi tahu waktu selesai.
  2. Callback: fungsi yang dipanggil waktu aksi selesai. Bikin kode bersarang dalam dan susah nanganin error.
  3. Promise: objek yang mewakili nilai masa depan. .then() buat sukses, .catch() buat gagal, keduanya ngembaliin promise baru jadi bisa dirantai. Promise.resolve, Promise.reject, Promise.all.
  4. async/await: nulis kode asinkron kaya sinkron. Fungsi async selalu ngembaliin promise; await nunggu promise resolve (atau ngelempar exception kalau reject).
  5. Generator (function* + yield): fungsi yang bisa dijeda dan dilanjutin, gampang buat nulis iterator. async sebenernya jenis generator khusus.
  6. Event loop: JavaScript cuma jalanin satu hal dalam satu waktu; callback masuk antrean. Kode lambat bisa nunda event lain. Promise selalu resolve sebagai event baru (setelah script sekarang selesai).
  7. Bug asinkron: hati-hati sama “celah” di antara await. Ngitung nilai baru (map + join) lebih aman daripada ngubah binding (+=) di dalam kode asinkron.

Latihan

Waktu Sepi

Ada kamera keamanan dekat lab Carla yang diaktifin sensor gerak. Dia terhubung ke jaringan dan mulai ngirim stream video waktu dia aktif. Karena dia lebih suka gak ketahuan, Carla udah nyiapin sistem yang nyadar jenis lalu lintas jaringan nirkabel ini dan nyalain lampu di sarangnya setiap kali ada aktivitas di luar, jadi dia tahu kapan harus diam.

Dia juga udah ngelog waktu-waktu di mana kameranya terpicu selama beberapa waktu dan mau pakai informasi ini buat memvisualisasikan waktu mana, dalam seminggu rata-rata, yang cenderung sepi dan mana yang cenderung sibuk. Log-nya disimpan di file yang megang satu angka timestamp (kaya yang dikembaliin Date.now()) per baris.

1695709940692
1695701068331
1695701189163

File "camera_logs.txt" megang daftar logfile. Tulis fungsi asinkron activityTable(day) yang buat hari tertentu dalam seminggu ngembaliin array 24 angka, satu buat setiap jam dalam hari, yang megang jumlah observasi lalu lintas jaringan kamera yang keliatan di jam itu. Hari diidentifikasi dengan angka pakai sistem yang dipakai Date.getDay, di mana Minggu adalah 0 dan Sabtu adalah 6.

Fungsi activityGraph, yang disediain sandbox, ngeringkas tabel kaya gitu jadi string.

Buat baca file, pakai fungsi textFile yang didefinisikan sebelumnya, dikasih nama file, dia ngembaliin promise yang resolve ke isi file. Ingat bahwa new Date(timestamp) bikin objek Date buat waktu itu, yang punya method getDay dan getHours yang ngembaliin hari dalam seminggu dan jam dalam hari.

Kedua jenis file, daftar logfile dan logfile-nya sendiri, punya setiap potongan data di barisnya sendiri, dipisah karakter newline ("\n").

async function activityTable(day) {
  let logFileList = await textFile("camera_logs.txt");
  // Kodemu di sini
}
 
activityTable(1)
  .then(table => console.log(activityGraph(table)));

Petunjuk:

Kamu bakal perlu ngonversi isi file-file ini jadi array. Cara termudah adalah pakai method split di string yang dihasilin textFile. Catat bahwa buat logfile-nya, itu masih bakal ngasih kamu array string, yang harus kamu konversi jadi angka sebelum ngasihnya ke new Date.

Ngeringkas semua titik waktu jadi tabel jam bisa dilakuin dengan bikin tabel (array) yang megang angka buat setiap jam dalam hari. Kamu lalu bisa ngeloop semua timestamp (di logfile dan angka di setiap logfile) dan buat masing-masing, kalau dia terjadi di hari yang benar, ambil jam terjadinya, dan tambahin satu ke angka yang sesuai di tabel.

Pastiin kamu pakai await di hasil fungsi asinkron sebelum ngelakuin apa pun dengannya, atau kamu bakal berakhir dengan Promise di tempat kamu ngarepin string.

Solusi:

async function activityTable(day) {
  let table = [];
  for (let i = 0; i < 24; i++) table[i] = 0;
 
  let logFileList = await textFile("camera_logs.txt");
  for (let filename of logFileList.split("\n")) {
    let log = await textFile(filename);
    for (let timestamp of log.split("\n")) {
      let date = new Date(Number(timestamp));
      if (date.getDay() == day) {
        table[date.getHours()]++;
      }
    }
  }
 
  return table;
}
 
activityTable(1)
  .then(table => console.log(activityGraph(table)));

Promise Beneran

Tulis ulang fungsi dari latihan sebelumnya tanpa async/await, pakai method Promise biasa.

function activityTable(day) {
  // Kodemu di sini
}
 
activityTable(6)
  .then(table => console.log(activityGraph(table)));

Dalam gaya ini, pakai Promise.all bakal lebih praktis daripada nyoba memodelkan loop di logfile-nya. Di fungsi async, cuma pakai await di loop lebih sederhana. Kalau baca file makan waktu, mana dari dua pendekatan ini yang bakal makan waktu paling sedikit buat jalan?

Kalau salah satu file yang terdaftar di daftar file punya typo, dan bacanya gagal, gimana kegagalan itu berakhir di objek Promise yang dikembaliin fungsimu?

Petunjuk:

Pendekatan paling lugas buat nulis fungsi ini adalah pakai rantai pemanggilan then. Promise pertama dihasilin dengan baca daftar logfile. Callback pertama bisa mecah daftar ini dan me-map textFile di dia buat dapet array promise buat dikasih ke Promise.all. Dia bisa ngembaliin objek yang dikembaliin Promise.all, jadi apa pun yang itu kembaliin jadi hasil dari nilai kembalian then pertama ini.

Kita sekarang punya promise yang ngembaliin array logfile. Kita bisa manggil then lagi di itu, dan naruh logika ngitung-timestamp di sana. Sesuatu kaya gini:

function activityTable(day) {
  return textFile("camera_logs.txt").then(files => {
    return Promise.all(files.split("\n").map(textFile));
  }).then(logs => {
    // analisis...
  });
}

Atau kamu bisa, buat penjadwalan kerja yang lebih baik lagi, naruh analisis setiap file di dalam Promise.all, biar kerjaan itu bisa dimulai buat file pertama yang balik dari disk, bahkan sebelum file lain balik.

Ini nunjukin bahwa cara kamu nyusun promise-mu bisa punya efek nyata ke cara kerjaannya dijadwalkan. Loop sederhana dengan await di dalamnya bakal bikin prosesnya sepenuhnya linier, dia nunggu setiap file dimuat sebelum lanjut. Promise.all bikin mungkin buat beberapa tugas secara konseptual dikerjain di saat yang sama, ngebolehin mereka maju sementara file masih dimuat. Ini bisa lebih cepat, tapi juga bikin urutan hal-hal bakal terjadi kurang bisa ditebak. Di kasus ini, kita cuma bakal naikin angka di tabel, yang gak susah dilakuin dengan cara aman. Buat jenis masalah lain, mungkin jauh lebih susah.

Waktu file di daftar gak ada, promise yang dikembaliin textFile bakal rejected. Karena Promise.all reject kalau promise apa pun yang dikasih ke dia gagal, nilai kembalian callback yang dikasih ke then pertama juga bakal jadi promise yang rejected. Itu bikin promise yang dikembaliin then gagal, jadi callback yang dikasih ke then kedua bahkan gak dipanggil, dan promise yang rejected dikembaliin dari fungsinya.

Solusi:

function activityTable(day) {
  let table = [];
  for (let i = 0; i < 24; i++) table[i] = 0;
 
  return textFile("camera_logs.txt").then(files => {
    return Promise.all(files.split("\n").map(name => {
      return textFile(name).then(log => {
        for (let timestamp of log.split("\n")) {
          let date = new Date(Number(timestamp));
          if (date.getDay() == day) {
            table[date.getHours()]++;
          }
        }
      });
    }));
  }).then(() => table);
}
 
activityTable(6)
  .then(table => console.log(activityGraph(table)));

Ngebangun Promise.all

Kaya yang kita lihat, dikasih array promise, Promise.all ngembaliin promise yang nunggu semua promise di array selesai. Dia lalu sukses, ngasih array nilai hasil. Kalau promise di array gagal, promise yang dikembaliin all juga gagal, nerusin alasan kegagalan dari promise yang gagal.

Implementasikan sesuatu kaya ini sendiri sebagai fungsi biasa bernama Promise_all.

Ingat bahwa setelah promise sukses atau gagal, dia gak bisa sukses atau gagal lagi, dan pemanggilan lebih lanjut ke fungsi yang me-resolve-nya diabaikan. Ini bisa menyederhanakan cara kamu nanganin kegagalan promise-mu.

function Promise_all(promises) {
  return new Promise((resolve, reject) => {
    // Kodemu di sini.
  });
}
 
// Kode tes.
Promise_all([]).then(array => {
  console.log("This should be []:", array);
});
function soon(val) {
  return new Promise(resolve => {
    setTimeout(() => resolve(val), Math.random() * 500);
  });
}
Promise_all([soon(1), soon(2), soon(3)]).then(array => {
  console.log("This should be [1, 2, 3]:", array);
});
Promise_all([soon(1), Promise.reject("X"), soon(3)])
  .then(array => {
    console.log("We should not get here");
  })
  .catch(error => {
    if (error != "X") {
      console.log("Unexpected failure:", error);
    }
  });

Petunjuk:

Fungsi yang dikasih ke constructor Promise bakal harus manggil then di setiap promise di array yang dikasih. Waktu salah satunya sukses, dua hal perlu terjadi. Nilai hasilnya perlu disimpen di posisi yang benar di array hasil, dan kita harus ngecek apakah ini promise pending terakhir dan nyelesaiin promise kita sendiri kalau iya.

Yang terakhir bisa dilakuin dengan counter yang diinisialisasi ke panjang array input dan dari mana kita kurangi 1 setiap kali promise sukses. Waktu dia nyampe 0, kita selesai. Pastiin kamu memperhitungkan situasi di mana array input-nya kosong (dan jadi gak ada promise yang bakal pernah resolve).

Nanganin kegagalan butuh sedikit pemikiran tapi ternyata sangat sederhana. Cukup kasih fungsi reject dari promise pembungkusnya ke setiap promise di array sebagai handler catch atau sebagai argumen kedua ke then supaya kegagalan di salah satu dari mereka memicu rejection dari seluruh promise pembungkusnya.

Solusi:

function Promise_all(promises) {
  return new Promise((resolve, reject) => {
    let results = [];
    let pending = promises.length;
    for (let i = 0; i < promises.length; i++) {
      promises[i].then(result => {
        results[i] = result;
        pending--;
        if (pending == 0) resolve(results);
      }).catch(reject);
    }
    if (promises.length == 0) resolve(results);
  });
}
 
// Kode tes.
Promise_all([]).then(array => {
  console.log("This should be []:", array);
});
function soon(val) {
  return new Promise(resolve => {
    setTimeout(() => resolve(val), Math.random() * 500);
  });
}
Promise_all([soon(1), soon(2), soon(3)]).then(array => {
  console.log("This should be [1, 2, 3]:", array);
});
Promise_all([soon(1), Promise.reject("X"), soon(3)]).then(array => {
  console.log("We should not get here");
}).catch(error => {
  if (error != "X") {
    console.log("Unexpected failure:", error);
  }
});