JavaScript Next Gen

Async Programming

Memahami Promise dan async/await untuk menangani operasi asinkron di JavaScript

Pengenalan Asynchronous Programming

JavaScript adalah single-threaded, tetapi dapat menangani operasi asinkron menggunakan event loop. Operasi seperti HTTP requests, file operations, dan timers tidak memblokir eksekusi kode lainnya.

Mengapa Perlu Async?

  • Permintaan jaringan: Fetch data dari API
  • Operasi file: Baca/tulis file
  • Query database: Query database
  • Timers: setTimeout, setInterval
  • Interaksi pengguna: Event klik, scroll

Callback Hell

Sebelum Promises, operasi async menggunakan callbacks yang bisa menjadi nested dan sulit dibaca (callback hell). ES6 Promises dan ES8 async/await mengatasi masalah ini.

Promises

Promise adalah objek yang merepresentasikan penyelesaian (atau kegagalan) eventual dari operasi asinkron.

Status Promise

StatusDeskripsi
PendingStatus awal, belum fulfilled atau rejected
FulfilledOperasi selesai dengan sukses
RejectedOperasi gagal

Membuat Promise

const promise = new Promise((resolve, reject) => {
  // Operasi async
  setTimeout(() => {
    const success = true;
    
    if (success) {
      resolve("Operasi berhasil!");
    } else {
      reject(new Error("Operasi gagal!"));
    }
  }, 1000);
});

Menggunakan Promises

promise
  .then(result => {
    console.log(result); // "Operasi berhasil!"
  })
  .catch(error => {
    console.error(error); // Tangani error
  });
promise
  .then(result => console.log(result))
  .catch(error => console.error(error))
  .finally(() => {
    console.log("Pembersihan"); // Selalu dijalankan
  });
fetchUser(1)
  .then(user => {
    console.log(user);
    return fetchPosts(user.id);
  })
  .then(posts => {
    console.log(posts);
    return fetchComments(posts[0].id);
  })
  .then(comments => {
    console.log(comments);
  })
  .catch(error => {
    console.error(error);
  });

Method Promise

Promise.all()

Menunggu semua promises selesai. Rejected jika salah satu gagal.

const promise1 = fetch('/api/users');
const promise2 = fetch('/api/posts');
const promise3 = fetch('/api/comments');

Promise.all([promise1, promise2, promise3])
  .then(([users, posts, comments]) => {
    // Semua resolved
    console.log(users, posts, comments);
  })
  .catch(error => {
    // Ada yang rejected
    console.error(error);
  });

Promise.race()

Resolved/rejected dengan hasil promise pertama yang selesai.

const timeout = new Promise((_, reject) => 
  setTimeout(() => reject(new Error('Timeout')), 5000)
);

const fetchData = fetch('/api/data');

Promise.race([fetchData, timeout])
  .then(data => console.log(data))
  .catch(error => console.error(error));

Promise.allSettled()

Menunggu semua promises selesai, terlepas dari sukses/gagal.

Promise.allSettled([promise1, promise2, promise3])
  .then(results => {
    results.forEach(result => {
      if (result.status === 'fulfilled') {
        console.log('Sukses:', result.value);
      } else {
        console.log('Gagal:', result.reason);
      }
    });
  });

Promise.any()

Resolved dengan promise pertama yang berhasil.

Promise.any([promise1, promise2, promise3])
  .then(result => {
    console.log('Sukses pertama:', result);
  })
  .catch(error => {
    console.log('Semua gagal');
  });

Implementasi Promises

Tambahkan ke js/app.js:

js/app.js
// ----------------------------
// Promises
// ----------------------------
export function demoPromises() {
  // Simulasi operasi asinkron dengan Promise
  function fetchData(id) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        if (id > 0) {
          resolve({ id, name: `User ${id}`, success: true });
        } else {
          reject(new Error("ID tidak valid"));
        }
      }, 1000);
    });
  }

  // Penggunaan Promise dasar
  let basicPromiseResult = "Memuat...";
  fetchData(1)
    .then(data => {
      basicPromiseResult = `Sukses: ${JSON.stringify(data)}`;
      document.getElementById('promise-basic').textContent = basicPromiseResult;
    })
    .catch(err => {
      basicPromiseResult = `Error: ${err.message}`;
      document.getElementById('promise-basic').textContent = basicPromiseResult;
    });

  // Promise chaining
  let chainResult = "Memuat...";
  fetchData(2)
    .then(user => {
      chainResult = `Dapat user: ${user.name}`;
      return fetchData(3); // Kembalikan promise lain
    })
    .then(secondUser => {
      chainResult += ` dan ${secondUser.name}`;
      document.getElementById('promise-chain').textContent = chainResult;
    })
    .catch(err => {
      chainResult = `Error: ${err.message}`;
      document.getElementById('promise-chain').textContent = chainResult;
    });

  // Promise.all
  let allResult = "Memuat...";
  Promise.all([fetchData(4), fetchData(5), fetchData(6)])
    .then(results => {
      allResult = `Semua selesai: ${results.map(r => r.name).join(', ')}`;
      document.getElementById('promise-all').textContent = allResult;
    })
    .catch(err => {
      allResult = `Error di salah satu: ${err.message}`;
      document.getElementById('promise-all').textContent = allResult;
    });

  // Promise.race
  let raceResult = "Memuat...";
  Promise.race([
    fetchData(7),
    fetchData(8),
    new Promise((_, reject) => 
      setTimeout(() => reject(new Error("Timeout")), 1500))
  ])
    .then(winner => {
      raceResult = `Pemenang race: ${winner.name}`;
      document.getElementById('promise-race').textContent = raceResult;
    })
    .catch(err => {
      raceResult = `Error race: ${err.message}`;
      document.getElementById('promise-race').textContent = raceResult;
    });

  // Buat elemen untuk update promise
  const outputDiv = document.createElement('div');
  outputDiv.className = 'promise-outputs';

  const createPromiseElement = (id, label) => {
    const el = document.createElement('div');
    el.className = 'promise-result';
    el.innerHTML = `<strong>${label}:</strong> <span id="${id}">Memuat...</span>`;
    outputDiv.appendChild(el);
  };

  createPromiseElement('promise-basic', 'Promise Dasar');
  createPromiseElement('promise-chain', 'Promise Chain');
  createPromiseElement('promise-all', 'Promise.all');
  createPromiseElement('promise-race', 'Promise.race');

  return outputDiv;
}

Async/Await

Async/await adalah syntactic sugar di atas Promises yang membuat kode async terlihat dan berperilaku seperti kode synchronous.

Sintaks Dasar

// Fungsi harus ditandai dengan 'async'
async function fetchUser(id) {
  // 'await' menghentikan eksekusi sampai promise resolved
  const response = await fetch(`/api/users/${id}`);
  const user = await response.json();
  return user;
}

Penanganan Error dengan try/catch

async function fetchUserSafe(id) {
  try {
    const response = await fetch(`/api/users/${id}`);
    
    if (!response.ok) {
      throw new Error('User tidak ditemukan');
    }
    
    const user = await response.json();
    return user;
  } catch (error) {
    console.error('Error:', error.message);
    return null;
  }
}

Eksekusi Paralel

// Berurutan - lambat (total 3 detik)
async function fetchSequential() {
  const user = await fetchUser(1);      // 1 detik
  const posts = await fetchPosts(1);    // 1 detik
  const comments = await fetchComments(1); // 1 detik
  
  return { user, posts, comments };
}
// Paralel - cepat (total 1 detik)
async function fetchParallel() {
  const [user, posts, comments] = await Promise.all([
    fetchUser(1),
    fetchPosts(1),
    fetchComments(1)
  ]);
  
  return { user, posts, comments };
}

Berurutan vs Paralel

Gunakan berurutan (await berturut-turut) ketika operasi kedua bergantung pada hasil operasi pertama. Gunakan paralel (Promise.all) ketika operasi independen untuk performa lebih baik.

Implementasi Async/Await

Tambahkan ke js/app.js:

js/app.js
// ----------------------------
// Async/Await
// ----------------------------
export function demoAsyncAwait() {
  // Simulasi API call
  function fetchUser(id) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        if (id > 0) {
          resolve({ 
            id, 
            name: `User ${id}`, 
            email: `user${id}@example.com` 
          });
        } else {
          reject(new Error("ID user tidak valid"));
        }
      }, 1000);
    });
  }

  function fetchUserPosts(userId) {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve([
          { id: 1, title: "Post 1", body: "Konten untuk post 1" },
          { id: 2, title: "Post 2", body: "Konten untuk post 2" },
        ]);
      }, 800);
    });
  }

  // Menggunakan async/await
  async function getUserData(id) {
    try {
      const user = await fetchUser(id);
      const posts = await fetchUserPosts(user.id);
      return {
        user,
        posts,
        success: true
      };
    } catch (error) {
      return {
        error: error.message,
        success: false
      };
    }
  }

  // Eksekusi paralel dengan async/await
  async function getMultipleUsers(ids) {
    try {
      // Jalankan promises secara paralel
      const userPromises = ids.map(id => fetchUser(id));
      const users = await Promise.all(userPromises);
      
      // Dapatkan posts untuk semua user secara paralel
      const postPromises = users.map(user => fetchUserPosts(user.id));
      const allPosts = await Promise.all(postPromises);
      
      // Gabungkan hasil
      return users.map((user, i) => ({
        user,
        posts: allPosts[i]
      }));
    } catch (error) {
      return { error: error.message };
    }
  }

  // Buat elemen untuk menampilkan hasil
  const outputDiv = document.createElement('div');
  outputDiv.className = 'async-outputs';

  // Contoh async/await dasar
  const basicAsyncResult = document.createElement('div');
  basicAsyncResult.innerHTML = '<strong>User Tunggal:</strong> Memuat...';
  basicAsyncResult.className = 'async-result';
  outputDiv.appendChild(basicAsyncResult);

  // Contoh async/await paralel
  const parallelAsyncResult = document.createElement('div');
  parallelAsyncResult.innerHTML = '<strong>Beberapa User:</strong> Memuat...';
  parallelAsyncResult.className = 'async-result';
  outputDiv.appendChild(parallelAsyncResult);

  // Jalankan dan update UI
  getUserData(42).then(result => {
    if (result.success) {
      basicAsyncResult.innerHTML = `
        <strong>User Tunggal:</strong> ${result.user.name} | 
        Posts: ${result.posts.length}
      `;
    } else {
      basicAsyncResult.innerHTML = `<strong>Error:</strong> ${result.error}`;
    }
  });

  getMultipleUsers([101, 102, 103]).then(results => {
    if (!results.error) {
      parallelAsyncResult.innerHTML = `
        <strong>Beberapa User:</strong> 
        ${results.map(r => r.user.name).join(', ')} |
        Total posts: ${results.reduce((sum, r) => sum + r.posts.length, 0)}
      `;
    } else {
      parallelAsyncResult.innerHTML = `<strong>Error:</strong> ${results.error}`;
    }
  });

  return outputDiv;
}

Update main.js

Update js/main.js untuk menjalankan demo async:

js/main.js
// Update import
import { 
  demoVariables, 
  demoArrowFunctions, 
  demoTemplateLiterals, 
  demoDestructuring, 
  demoSpreadRest,
  demoDefaultParams,
  demoClasses,
  demoObjectLiterals,
  demoArrayMethods,
  demoAdvancedArrays,
  demoPromises,
  demoAsyncAwait
} from './app.js';

// Tambahkan ke fungsi runAllDemos
function runAllDemos() {
  // ... demo sebelumnya ...

  // Demo Promises
  addOutput(
    "11. Promises",
    "Penanganan operasi asinkron dengan Promise",
    ""
  );
  const promiseOutput = demoPromises();
  document.querySelector('.output-item:last-child .result').appendChild(promiseOutput);

  // Demo Async/Await
  addOutput(
    "12. Async/Await",
    "Penanganan operasi asinkron dengan syntax yang lebih bersih",
    ""
  );
  const asyncOutput = demoAsyncAwait();
  document.querySelector('.output-item:last-child .result').appendChild(asyncOutput);
}

Perbandingan: Callbacks vs Promises vs Async/Await

// Callback hell
fetchUser(1, (error, user) => {
  if (error) return console.error(error);
  
  fetchPosts(user.id, (error, posts) => {
    if (error) return console.error(error);
    
    fetchComments(posts[0].id, (error, comments) => {
      if (error) return console.error(error);
      console.log(comments);
    });
  });
});
// Promise chain
fetchUser(1)
  .then(user => fetchPosts(user.id))
  .then(posts => fetchComments(posts[0].id))
  .then(comments => console.log(comments))
  .catch(error => console.error(error));
// Async/await - paling mudah dibaca
async function getData() {
  try {
    const user = await fetchUser(1);
    const posts = await fetchPosts(user.id);
    const comments = await fetchComments(posts[0].id);
    console.log(comments);
  } catch (error) {
    console.error(error);
  }
}

Praktik Terbaik

1. Selalu Tangani Error

// Baik
async function fetchData() {
  try {
    const data = await fetch('/api/data');
    return await data.json();
  } catch (error) {
    console.error('Error:', error);
    return null;
  }
}

// Hindari
async function fetchData() {
  const data = await fetch('/api/data'); // Error tidak tertangani
  return await data.json();
}

2. Gunakan Promise.all untuk Operasi Independen

// Baik - paralel (cepat)
const [users, posts] = await Promise.all([
  fetchUsers(),
  fetchPosts()
]);

// Hindari - berurutan (lambat)
const users = await fetchUsers();
const posts = await fetchPosts();

3. Hindari Mencampur Promises dan Async/Await

// Baik - konsisten dengan async/await
async function getData() {
  const user = await fetchUser(1);
  const posts = await fetchPosts(user.id);
  return { user, posts };
}

// Hindari - mencampur gaya
async function getData() {
  const user = await fetchUser(1);
  return fetchPosts(user.id) // Mengembalikan promise, bukan nilai
    .then(posts => ({ user, posts }));
}

4. Kembalikan Promises dari Fungsi Async

// Baik - return eksplisit
async function processData() {
  const data = await fetchData();
  return processResult(data);
}

// Berfungsi tapi kurang jelas
async function processData() {
  const data = await fetchData();
  processResult(data); // Return implisit undefined
}

Kapan Menggunakan Apa?

SkenarioRekomendasiAlasan
Operasi async sederhanaAsync/awaitPaling mudah dibaca
Beberapa operasi independenPromise.allPerforma terbaik
Salah satu dari banyak harus berhasilPromise.race/anyRespons pertama menang
Semua harus selesai terlepas hasilnyaPromise.allSettledPerlu semua hasil
Bekerja dengan API Promise yang ada.then()/.catch()Konsistensi dengan library
Prioritas penanganan errortry/catch dengan async/awaitAlur kontrol jelas

Standar Modern

Async/await adalah standar modern untuk operasi async di JavaScript. Gunakan ini sebagai pilihan default kecuali ada alasan khusus untuk menggunakan Promises langsung.

Testing Kode Async

// Dengan Jest atau framework testing serupa
describe('Fungsi async', () => {
  test('fetchUser mengembalikan data user', async () => {
    const user = await fetchUser(1);
    expect(user).toHaveProperty('id');
    expect(user).toHaveProperty('name');
  });
  
  test('fetchUser menangani error', async () => {
    await expect(fetchUser(-1)).rejects.toThrow('ID user tidak valid');
  });
  
  test('fetching paralel bekerja', async () => {
    const start = Date.now();
    await Promise.all([fetchUser(1), fetchUser(2), fetchUser(3)]);
    const duration = Date.now() - start;
    
    // Harus lebih cepat dari berurutan (3+ detik)
    expect(duration).toBeLessThan(2000);
  });
});

Pertimbangan Performa

1. Hindari await yang Tidak Perlu

// await tidak perlu
async function getTotal() {
  return await calculateTotal(); // await ekstra
}

// Lebih baik - return implisit
async function getTotal() {
  return calculateTotal();
}

2. Gunakan Promise.all untuk Operasi Independen

// Lambat - total 3 detik
async function slow() {
  const a = await operation1(); // 1 detik
  const b = await operation2(); // 1 detik
  const c = await operation3(); // 1 detik
  return [a, b, c];
}

// Cepat - total 1 detik
async function fast() {
  const [a, b, c] = await Promise.all([
    operation1(),
    operation2(),
    operation3()
  ]);
  return [a, b, c];
}

3. Pertimbangkan Caching

const cache = new Map();

async function fetchUserCached(id) {
  if (cache.has(id)) {
    return cache.get(id);
  }
  
  const user = await fetchUser(id);
  cache.set(id, user);
  return user;
}

Dukungan Browser

FiturChromeFirefoxSafariEdge
Promises32+29+8+12+
Async/Await55+52+10.1+15+
Promise.allSettled76+71+13+79+
Promise.any85+79+14+85+

Transpilasi

Untuk mendukung browser lama, gunakan transpiler seperti Babel dengan polyfills untuk mengkonversi async/await ke promises atau generator functions.

Ringkasan

Poin Penting

  1. Promises mengatasi callback hell dengan chaining
  2. Async/await membuat kode async terlihat synchronous
  3. Promise.all untuk eksekusi paralel
  4. try/catch untuk penanganan error dengan async/await
  5. Selalu tangani error di kode async
  6. Lebih suka async/await untuk keterbacaan

Langkah Selanjutnya

  • Praktik dengan API nyata (fetch GitHub API, weather API, dll.)
  • Implementasi retry logic dan pola timeout
  • Pelajari async generators dan for-await-of
  • Jelajahi Web Workers untuk task CPU-intensive
  • Pelajari service workers untuk kemampuan offline

Latihan

  1. Buat fungsi yang fetch data dari beberapa endpoint dan gabungkan hasilnya
  2. Implementasi mekanisme retry dengan exponential backoff
  3. Buat loading states untuk operasi async di UI
  4. Bangun error boundary untuk error async
  5. Implementasi pagination dengan async/await
  6. Buat debounced search dengan async API calls