Promise : Sebuah Janji Eksekusi dari Javascript

Judul artikel ini mungkin agak sedikit bombastis: "Promise : Sebuah Janji Eksekusi dari Javascript". Promise adalah sebuah mekanisme dari standar ECMAScript 2015 yang memungkinkan kita melakukan eksekusi kode fungsi Javascript asynchronous (salah satunya adalah request AJAX) dan mendapatkan nilai balik (return value) dari eksekusi kode tersebut tidak secara langsung, melainkan berupa objek "Promise" yang menjanjikan eksekusi di masa yang akan datang! Paham? Tidak? kalau teman-teman pembaca tidak paham itu wajar, saya juga awalnya bingung kenapa pula ini ada fitur di bahasa pemrograman pake "janji-janji" segala!? Mari kita tengok definisi dari Promise yang saya kutip dari dokumentasi Mozilla Developer Network :

A Promise is a proxy for a value not necessarily known when the promise is created. It allows you to associate handlers with an asynchronous action's eventual success value or failure reason. This lets asynchronous methods return values like synchronous methods: instead of immediately returning the final value, the asynchronous method returns a promise to supply the value at some point in the future.

Masih belum mudeng? sama saya juga hahaha🤣🤣🤣! Kalau begitu mari kita lihat contoh kode HTML dan Javascript sebagai berikut sebagai berikut:

promise.html

<!doctype html>
<html>
<head><title>Promise</title><meta charset="UTF-8">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
<script>
    var datauser = [];
    // fungsi untuk melakukan request AJAX
    function getUsers(url) {
      $.ajax({
        url: url,
        method: 'get'
      }).done(function(hasil) {
        // isi variabel global datauser dengan hasil dari AJAX
        datauser = hasil;
      });
    }
    // fungsi untuk mengubah data JSON ke list HTML
    function ubahDataKeHTML(datauser) {
        var html = '<ul>';
        $.each( datauser, function( key, value ) {
            html += '<li>'+value.name+' - '+value.email+'</li>'
        });
        html += '</ul>';
        return html;
    }
    // panggil fungsi 'getUsers'
    getUsers('https://jsonplaceholder.typicode.com/users');
    // ubah data dari hasil AJAX ke list HTML
    var datauserHTML = ubahDataKeHTML(datauser);
    // tampilkan isi variabel ke log
    console.log(datauser);
    console.log(datauserHTML);
    // tampilkan data user
    $(document).ready(function() {
        $('.container').html(datauserHTML);
    });
</script>
</head>
<body>
    <div class="container"></div>
</body>
</html>

Hasil yang muncul adalah sebagai berikut:

Apa yang terjadi? ternyata tidak sesuai apa yang kita harapkan! tag div kita tidak terisi dengan list data user seperti yang kita mau, dan juga ternyata variabel array datauser tidak terisi dengan data padahal tidak ada yang salah dengan kode ini, semua berjalan dengan baik. saya yakin banyak dari teman-teman web programmer yang pernah mengalami hal ini dan garuk-garuk kepala, lalu browsing cari jawaban di Stackoverflow kan? hehehe :D.

Loh mas ngapain ribet, heeellllooowwww?? ubah aja kode-nya, misalnya lakukan eksekusi fungsi ubahDataKeHTML ke dalam fungsi .done dari objek $.ajax()? atau ngapain juga bikin fungsi-fungsi segala?

Hehehe, silahkan saja dicoba, paling nanti ada saatnya kepentok lagi hehehe 😬😬😬. Kegagalan kode di atas terjadi karena eksekusi kode $.ajax dan .done berjalan secara asynchronous/paralel dan tidak terjadi secara berurutan sehingga variabel global datauser mungkin belum terisi dengan hasil dari request AJAX karena request AJAX belum selesai. Penyebab lain adalah karena Javascript hanya bisa mengakses variabel global satu tingkat di atas cakupan fungsi, dalam hal kode di atas variabel datauser berarti dua tingkat di atas cakupan fungsi .done. Cara yang elegan untuk masalah ini adalah dengan menggunakan Promise yang sudah menjadi standar default sejak spesifikasi ECMAScript 2015 atau serin disingkat ES2015. Berikut adalah kode Javascript yang sudah kita ubah dengan memanfaatkan Promise:

promise1.html

<!doctype html>
<html>
<head><title>Promise</title><meta charset="UTF-8">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
<script>
    // fungsi untuk melakukan request AJAX dan mengembalikan objek Promise
    function getUsers(url) {
      return new Promise(function(resolve, reject) {
        $.ajax({
            url: url,
            method: 'get'
        }).done(function(hasil) {
            // simpan hasil dari AJAX ke callback 'resolve' dari Promise
            // untuk kemudian nanti dipakai oleh fungsi '.then'
            resolve(hasil);
        });
      })
    }
    // fungsi untuk mengubah data JSON ke list HTML
    function ubahDataKeHTML(datauser) {
        var html = '<ul>';
        $.each( datauser, function( key, value ) {
            html += '<li>'+value.name+' - '+value.email+'</li>'
        });
        html += '</ul>';
        return html;
    }
    // panggil fungsi 'getUsers' dan jalankan fungsi '.then'
    // argumen dari fungsi '.then' adalah sebuah callback dengan argumen 'hasil'
    // yang berisikan objek JSON hasil AJAX
    getUsers('https://jsonplaceholder.typicode.com/users').then(function(hasil) {
        console.log(hasil);
        console.log('Janji telah dipenuhi!');
        var datauserHTML = ubahDataKeHTML(hasil);
        // tampilkan data user
        $(document).ready(function() {
            $('.container').html(datauserHTML);
        });
    });
</script>
</head>
<body>
    <div class="container"></div>
</body>
</html>

Dengan kode ini kita akan melihat hasil seperti berikut ini:

Penjelasan sederhana dari kode ini adalah, ketika kita menggunakan Promise, maka kita menggunakan callback resolve untuk menyimpan hasil dari request AJAX kita, yang kemudian hasil ini akan tersedia pada argumen callback fungsi .then untuk selanjutnya diolah dan dijadikan HTML oleh fungsi ubahDataKeHTML.

Chaining

Salah satu kelebihan dari Promise adalah memungkinkan terjadinya chaining atau eksekusi Promise berantai, fitur ini berguna ketika kita ingin melakukan eksekusi kode secara berantai dimana eksekusi kode dilakukan benar-benar setelah eksekusi kode sebelumnya sudah selesai atau terpenuhi, seperti bisa kita lihat pada kode berikut:

promise2.html

<!doctype html>
<html>
<head><title>Promise</title><meta charset="UTF-8">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css" integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" crossorigin="anonymous">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
<script>
    // fungsi untuk melakukan request AJAX dan mengembalikan objek Promise
    function getUsers(url) {
      return new Promise(function(resolve, reject) {
        $.ajax({
            url: url,
            method: 'get'
        }).done(function(hasil) {
            // simpan hasil dari AJAX ke callback 'resolve' dari Promise
            // untuk kemudian nanti dipakai oleh fungsi '.then'
            resolve(hasil);
        }).fail(function() {
            reject('Error pada request AJAX!');         
        });
      })
    }
    // fungsi untuk mengubah data JSON ke list dan tabel HTML
    function ubahDataKeHTML(datauser) {
        console.log('Janji pertama (request AJAX) telah dipenuhi');
        var list = '<h3>Data users dalam format list</h3>';
        list += '<ul>';
        var tabel = '<h3>Data users dalam format tabel</h3>';
        tabel += '<table class="table table-bordered">';
        $.each( datauser, function( key, value ) {
            list += '<li>'+value.name+' - '+value.email+'</li>'
            tabel += '<tr><td>'+value.name+'</td><td>'+value.email+'</td></tr>'
        });
        list += '</ul>';
        tabel += '</table>';
        return {htmlList: list, htmlTable: tabel};
    }
    // fungsi untuk menginject HTML ke dalam div .container
    function injectHTML(html) {
        console.log('Janji kedua (ubah JSON ke HTML) telah dipenuhi');
        return $('.container').append(html.htmlList).append(html.htmlTable);
    }
    // fungsi untuk menampilkan log pada console
    function terakhir(objJquery) {
        console.log('Janji terakhir (inject HTML ke .container) telah dipenuhi');
        console.log('Berikut objek jQuery dari nilai balik callback Janji (Promise) sebelumnya:');
        console.log(objJquery);
    }

    // setiap kali '.then' dipanggil maka akan mengembalikan objek Promise
    // dan nilai balik dari callback bisa diakses pada callback berikutnya 
    // yang bisa kita 'chain' tanpa batas
    // fungsi '.catch' menangkap hasil dari callback 'reject' yang apabila terjadi
    // maka Promise tidak bisa terpenuhi
    getUsers('https://jsonplaceholder.typicode.com/users')
    .then(hasil => ubahDataKeHTML(hasil))
    .then(html => injectHTML(html))
    .then(objJquery => terakhir(objJquery))
    .catch(error => { 
        console.log(error); 
        $('.container').html('<div class="alert alert-danger">Data users gagal diambil disebabkan oleh : '+error+'</div>'); 
        });
</script>
</head>
<body>
    <div class="container"></div>
</body>
</html>

Kita akan melihat hasil sebagai berikut untuk kode di atas:

Syntax alternatif untuk chaining Promise menggunakan standar ES2017 adalah dengan menggunakan async/await seperti berikut ini:

promise3.html

<script>
    ...

    // alternatif chaining dengan menggunakan 'async/await'
    async function ambilUserDanTampilkan(url) {
        try {
            let hasil = await getUsers(url);
            let html = await ubahDataKeHTML(hasil);
            let objJquery = await injectHTML(html);
            terakhir(objJquery);
        } catch(error) {
            console.log(error); 
            $('.container').html('<div class="alert alert-danger">Data users gagal diambil disebabkan oleh : '+error+'</div>'); 
        }     
    }
    // jalankan fungsi async
    ambilUserDanTampilkan('https://jsonplaceholder.typicode.com/users');

</script>

Promise.all

Ada kalanya dalam membangun aplikasi web interaktif dengan Javascript kita ingin suatu kode dieksekusi ketika semua persyaratan (eksekusi kode lain) sudah terpenuhi, tanpa memperdulikan urutan selesai-nya persyaratannya tersebut. Misalnya kita melakukan request AJAX ke banyak sumber yang berbeda dan setelah semua request AJAX selesai kita akan mengeksekusi kode terakhir yang menampilkan kotak alert menandakan bahwa semua request tersebut telah selesai, maka kita bisa menggunakan metode Promise.all dalam hal ini. Mari kita ilustrasikan dengan kode berikut ini:

promise-all.html

<!doctype html>
<html>
<head><title>Promise</title><meta charset="UTF-8">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css" integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" crossorigin="anonymous">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
<script>

    function autentikasiUser(url, username, password) {
      return new Promise(function(resolve, reject) {
        // lakukan request AJAX
        // kita umpamakan berhasil dan selesai dalam waktu 1,5 detik
        setTimeout(function(){
            resolve("Autentikasi user berhasil!"); console.log('Autentikasi berhasil'); }, 1500);
      });
    }
    
    function ambilDataBerita(url) {
      return new Promise(function(resolve, reject) {
        // lakukan request AJAX
        // kita umpamakan berhasil dan selesai dalam waktu 2,5 detik
        setTimeout(function(){
            resolve('berita terbaru berhasil diambil'); console.log('Berita berhasil diambil'); }, 2500);  
      });
    }

    function ambilDataCuaca(url) {
      return new Promise(function(resolve, reject) {
        // lakukan request AJAX
        // kita umpamakan berhasil dan selesai dalam waktu 3,5 detik
        setTimeout(function(){
            resolve('Data cuaca berhasil diambil'); console.log('Cuaca berhasil diambil'); }, 3500);
      });
    }

    let c = ambilDataCuaca('http://servercuaca.com');
    let a = autentikasiUser('http://serverautentikasi.com', 'dicarve', 'rahasia');
    let b = ambilDataBerita('http://serverberita.com');
    
    let pAll = Promise.all([c, b, a]).then(hasil => {
        console.log('Semua Promise (Janji) telah terpenuhi');
        $('.container .alert').removeClass('alert-info').addClass('alert-success').html('Semua data berhasil diambil dari server!');
    }, error => {
        console.log('Terjadi error karena salah satu Promise tidak bisa terpenuhi!')
        $('.container .alert').removeClass('alert-info').addClass('alert-danger').html('Error disebabkan oleh: '+error);
    });

</script>
</head>
<body>
    <div class="container"><div class="alert alert-info">Loading data...</div></div>
</body>
</html>

Ketika kita menggunakan Promise.all, argumen yang digunakan harus berupa Array yang mengandung semua fungsi Promise yang kita eksekusi. Apabila semua fungsi Promise ini berhasil dan terpenuhi (resolve), maka kode di dalam callback pertama fungsi .then akan dieksekusi. Sedangkan apabila salah satu saja gagal alias reject, maka callback kedua akan dijalankan dan pesan error akan muncul. Bagaimana? menarik bukan? sekali kita paham akan pemanfaatan Promise maka kemungkinan besar kita akan banyak memanfaatkannya untuk menuliskan kode yang lebih elegan untuk operasi-operasi asynchronous di kode Javascript kita. Selamat mencoba!

Komentar

Postingan populer dari blog ini

Template Aplikasi Web CRUD Sederhana dengan CodeIgniter

Membuat Aplikasi Berbasis Web "Single Page Application" dengan Vue.js (Bagian 1)

Frontend aplikasi web dengan AngularJS dan backend PHP