Atasi Callback Hell dengan Promise

Javascript adalah bahasa scripting yang serbaguna dan mudah difahami. JS banyak saya gunakan untuk pengembangan aplikasi web service, website dan juga 2D game programming. Meski begitu, ada beberapa hal yang fundamental yang harus difahami dalam penggunaannya.

Sifat paling menonjol dari Javascript adalah Asynchronous dan non-blocking. Misal saya gambarkan seperti ini

var asyncTask = 'connect db'; // connect db
 
console.log(asyncTask); // undefined

Ketika asyncTask di cetak dalam console, maka hasilnya adalah null atau undefined. Ini karena console dipanggil saat proses asyncTask belum selesai. Untuk menanggulangi kita bisa memakai beberapa pattern; Callback, Promise, Signal atau Event. Paling sederhana dan paling cocok untuk kasus seperti ini kita bisa menggunakan callback :

var asyncTask = function(callback){
  'connect db';
  // do callback 
};
 
asyncTask(function(err, result){
    // do with result or err
});

Ketika asyncTask dieksekusi menggunakan callback, secara async kita bisa mendapatkan object yang kita inginkan dengan seperti ‘blocking-thread‘. Namun, coba Anda bayangkan jika object yang Anda inginkan harus melalui prosedur asynctask yang berlapis, misal :

fs.readdir(source, function (err, files) {
  if (err) {
    console.log('Error finding files: ' + err)
  } else {
    files.forEach(function (filename, fileIndex) {
      console.log(filename)
      gm(source + filename).size(function (err, values) {
        if (err) {
          console.log('Error identifying file size: ' + err)
        } else {
          console.log(filename + ' : ' + values)
          aspect = (values.width / values.height)
          widths.forEach(function (width, widthIndex) {
            height = Math.round(width / aspect)
            console.log('resizing ' + filename + 'to ' + height + 'x' + height)
            this.resize(width, height).write(dest + 'w' + width + '_' + filename, function(err) {
              if (err) console.log('Error writing file: ' + err)
            })
          }.bind(this))
        }
      })
    })
  }
})

Hal semacam ini disebut Callback Hell. Apa itu Callback Hell ? Ada sebuah situs yang membahas dengan detail tentang fenomena ini, termasuk bagaimana cara mengakalinya; http://callbackhell.com/.

Jika aplikasi yang Anda buat banyak sekali berhubungan dengan database, apalagi dengan skema nosql, Callback Hell secara tidak sadar akan Anda temui -jika Anda memakai pattern callback- . Gunakan Promise!

Promise mulai ditambahkan pada ES6, namun begitu wacana tentang Promise sudah banyak dibahas sebelum ES6 diterbitkan yang memang di proposalkan untuk menggantikan Callback. Selangkapnya tentang Promise bisa Anda lihat disini : https://developer.mozilla.org.

Syntax dasar promise adalah sebagai berikut :

new Promise( /* executor */ function(resolve, reject) { ... } );

Biasanya bentuk eksekutornya adalah resolve dan reject atau dalam seperti dalam Callback biasa kita temukan misalnya err dan result.

Ada 3 state dalam Promise :

pending: initial state, kita bisa artikan proses async baru akan atau sedang berjalan

fulfilled: operasi telah selesai, biasanya dieksekusi oleh resolve.

rejected: operasi gagal, dieksekusi oleh reject.

Kita lihat diagram berikut :

Gambar disediakan oleh : mozilla.org

Pada blok paling akhir, kita hanya mendapatkan dua kondisi, .then dan .catch. Gampangnya seperti ini : Apapun yang di-resolve, maka akan masuk ke .then() dan apapun yang di-reject maka akan masuk ke .catch().

Ada 4 method yang bisa dilakukan Promise :

Promise.all(iterable) 
: ekesekusi beberapa Promise dengan result dari semua promise.

Promise.race(iterable) : eksekusi beberapa Promise dengan hasil promise yang paling cepat selesai

Promise.resolve(value) : Mengembalikan object hasil operasi.

Promise.reject(reason) : Mengembalikan object/penyebab operasi gagal

Kembali lagi ke Callback Hell. Beberapa API memang generic memakai Callback, misalnya pada query pada MongoDB memakai Mongo Native. Hal tersebut tidak bisa kita ubah, jalannya saja yang perlu kita ubah. Contoh sederhana :

function someFun(query){
  function someFun(query){
  return new Promise(function(resolve, reject){
    someProcess.hello(query, function(err, result){ // return callback
      if(err) ? reject(err) : resolve(result); // resolve and reject result
    });
  });
}
// call promise
someFun(someQuery).then(function(result){
  // do with result from resolve
}).catch(function (err){
  // catch err from reject
});

Bagaimana dengan operasi yang berlapis ? Perhatikan contoh berikut :

 

function someFun(query){
  return new Promise(function(resolve, reject){
    someProcess.hello(query, function(err, result){ // return callback
      if(err) ? reject(err) : resolve(result); // resolve and reject result
    });
  });
}
 
function anotherFun(query){
  return new Promise(function(resolve, reject){
    someProcess.world(query, function(err, result){ // return callback
      if(err) ? reject(err) : resolve(result); // resolve and reject result
    });
  });
}
 
// call promises
 
someFun(someQuery).then(function(result){
  // do with result from resolve
  anotherQuery.blah = result.blah;
  return anotherFun(anotherQuery);
}).then(function(result){
  // do with result from resolve
  console.log("result from anotherQuery : ", result);
}).catch(function err{
  // catch err from all reject
});

Contoh diatas adalah pemanggilan fungsi ke fungsi lainnya berdasarkan hasil dari fungsi pertama. Setelah fungsi pertama operasinya selesai dan return object, maka object tersebut akan dipakai sebagai parameter untuk pemanggilan fungsi yang kedua. Bagaimana jika antar Promise dalam pemanggilannya tidak memiliki relasi ? Misalnya mengambil semua data pada collection yang berbeda ? Bisa menggunakan Promise.all :

function someFun(){
  return new Promise(function(resolve, reject){
    someProcess.hello(function(err, result){ // return callback
      if(err) ? reject(err) : resolve(result); // resolve and reject result
    });
  });
}
 
function anotherFun(){
  return new Promise(function(resolve, reject){
    someProcess.world(function(err, result){ // return callback
      if(err) ? reject(err) : resolve(result); // resolve and reject result
    });
  });
}
 
// call all promises
 
Promise.all([someFun(), anotherFun()]).then(function(results){
  // results[0] for someFun(), results[1] for anotherFun()
}).catch(function(err){
  // catch err
});

Banyak yang bilang pattern callback memang sudah old skool. Namun beberapa API masih menggunakannya. Untuk kasus dengan state banyak, memang idealnya memakai event emitter, tergantung bagaimana Anda merancang pattern sesuai kebutuhan operasi. Pada penggunaan Promise, angat disarankan pada kasus-kasus yang identik seperti yang saya sebutkan diatas.