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
| Status | Deskripsi |
|---|---|
| Pending | Status awal, belum fulfilled atau rejected |
| Fulfilled | Operasi selesai dengan sukses |
| Rejected | Operasi 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:
// ----------------------------
// 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:
// ----------------------------
// 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:
// 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?
| Skenario | Rekomendasi | Alasan |
|---|---|---|
| Operasi async sederhana | Async/await | Paling mudah dibaca |
| Beberapa operasi independen | Promise.all | Performa terbaik |
| Salah satu dari banyak harus berhasil | Promise.race/any | Respons pertama menang |
| Semua harus selesai terlepas hasilnya | Promise.allSettled | Perlu semua hasil |
| Bekerja dengan API Promise yang ada | .then()/.catch() | Konsistensi dengan library |
| Prioritas penanganan error | try/catch dengan async/await | Alur 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
| Fitur | Chrome | Firefox | Safari | Edge |
|---|---|---|---|---|
| Promises | 32+ | 29+ | 8+ | 12+ |
| Async/Await | 55+ | 52+ | 10.1+ | 15+ |
| Promise.allSettled | 76+ | 71+ | 13+ | 79+ |
| Promise.any | 85+ | 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
- Promises mengatasi callback hell dengan chaining
- Async/await membuat kode async terlihat synchronous
- Promise.all untuk eksekusi paralel
- try/catch untuk penanganan error dengan async/await
- Selalu tangani error di kode async
- 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
- Buat fungsi yang fetch data dari beberapa endpoint dan gabungkan hasilnya
- Implementasi mekanisme retry dengan exponential backoff
- Buat loading states untuk operasi async di UI
- Bangun error boundary untuk error async
- Implementasi pagination dengan async/await
- Buat debounced search dengan async API calls
