Web Development

JavaScript Async/Await: Panduan Lengkap

Kuasai pemrograman asynchronous di JavaScript — dari Promise dasar hingga async/await, error handling, Promise.all/race/allSettled, Fetch API, dan pola async modern untuk aplikasi production

1. Pengenalan Asynchronous JavaScript

JavaScript bersifat single-threaded — hanya bisa menjalankan satu operasi dalam satu waktu. Namun, banyak operasi dalam web development yang membutuhkan waktu (network request, file reading, timer). Asynchronous programming memungkinkan JavaScript menjalankan operasi yang memakan waktu tanpa memblokir thread utama.

Synchronous vs Asynchronous

Diagram: Synchronous vs Asynchronous Execution
  SYNCHRONOUS (Blocking):
  ───────────────────────────────────────
  Task A ████░░░░░░░░░░ (selesai)
         Task B ████████░░░░ (selesai)
                Task C ████████ (selesai)
  ───────────────────────────────────────
  Total: A + B + C (ditambahkan)

  ASYNCHRONOUS (Non-Blocking):
  ───────────────────────────────────────
  Task A ████░░░░░░░░░░ (mulai, tunggu)
  Task B ████░░░░░░░░░░ (mulai, tunggu)
  Task C ████░░░░░░░░░░ (mulai)
  Callback A ← selesai
  Callback B ← selesai
  Callback C ← selesai
  ───────────────────────────────────────
  Total: MAX(A, B, C) (berjalan paralel)

Event Loop

// JavaScript Event Loop
// 1. Call Stack: Eksekusi kode synchronous
// 2. Web APIs: setTimeout, fetch, DOM events (di browser)
// 3. Callback Queue (Macro task): setTimeout, setInterval
// 4. Microtask Queue: Promise callbacks, queueMicrotask
// 5. Event Loop: Pindahkan task ke Call Stack saat kosong

console.log('1: Start');               // Call Stack

setTimeout(() => {
  console.log('2: setTimeout');         // Macro task queue
}, 0);

Promise.resolve().then(() => {
  console.log('3: Promise');            // Microtask queue
});

console.log('4: End');                  // Call Stack

// Output:
// 1: Start
// 4: End
// 3: Promise      ← Microtask diproses SEBELUM macro task
// 2: setTimeout   ← Macro task diproses terakhir

// Microtask selalu diproses sebelum Macrotask!
// Urutan: Sync code → Microtask → Macrotask

Timeline Evolusi Async JavaScript

Era Teknik Masalah
2009+CallbacksCallback Hell / Pyramid of Doom
2015 (ES6)PromisesChaining masih bisa rumit
2017 (ES8)Async/AwaitSyntax clean, seperti synchronous
2020 (ES2020)Promise.allSettledLebih fleksibel error handling
2021 (ES2021)Promise.anyResolusi dari promise pertama yang sukses
2022+Top-level awaitAwait di luar async function (modules)

2. Callbacks & Masalahnya

Callback adalah fungsi yang dikirim sebagai argumen ke fungsi lain, dan dipanggil ketika operasi selesai. Ini adalah pola async paling dasar.

Callback Dasar

// Callback sederhana
function fetchData(callback) {
  setTimeout(() => {
    const data = { id: 1, name: 'Budi' };
    callback(data);
  }, 1000);
}

fetchData((result) => {
  console.log(result); // { id: 1, name: 'Budi' }
});

// Callback dengan error (Node.js pattern)
function readFile(path, callback) {
  setTimeout(() => {
    if (path === '/valid') {
      callback(null, 'File content here');
    } else {
      callback(new Error('File not found'), null);
    }
  }, 500);
}

readFile('/valid', (err, data) => {
  if (err) {
    console.error('Error:', err.message);
    return;
  }
  console.log('Data:', data);
});

Callback Hell (Pyramid of Doom)

// ❌ CALLBACK HELL: Nested callbacks yang sulit dibaca
function getUser(userId, callback) {
  setTimeout(() => callback(null, { id: userId, name: 'Budi' }), 1000);
}

function getOrders(userId, callback) {
  setTimeout(() => callback(null, [{ orderId: 1 }]), 1000);
}

function getOrderDetails(orderId, callback) {
  setTimeout(() => callback(null, { orderId, product: 'Laptop' }), 1000);
}

function getProduct(productId, callback) {
  setTimeout(() => callback(null, { id: productId, price: 15000000 }), 1000);
}

// Nested callbacks — sulit dibaca dan di-maintain!
getUser(1, (err, user) => {
  if (err) return console.error(err);
  getOrders(user.id, (err, orders) => {
    if (err) return console.error(err);
    getOrderDetails(orders[0].orderId, (err, details) => {
      if (err) return console.error(err);
      getProduct(details.productId, (err, product) => {
        if (err) return console.error(err);
        console.log(product.price);
        // Kita sudah 4 level nesting!
      });
    });
  });
});

3. Memahami Promises

Promise adalah objek yang merepresentasikan hasil dari operasi asynchronous — bisa berhasil (resolved) atau gagal (rejected). Promise diperkenalkan di ES6 (2015) untuk mengatasi masalah callback hell.

Tiga State Promise

Diagram: Promise States
  ┌──────────────────────────────────────────────┐
  │              PROMISE LIFECYCLE                │
  │                                              │
  │  ┌──────────┐                                │
  │  │ PENDING  │ ← State awal                   │
  │  │(menunggu)│                                │
  │  └────┬─────┘                                │
  │       │                                      │
  │       ├──→ resolve(value)                    │
  │       │    ┌──────────────┐                  │
  │       │    │  FULFILLED   │ ← .then(value)   │
  │       │    │  (berhasil)  │                  │
  │       │    └──────────────┘                  │
  │       │                                      │
  │       └──→ reject(error)                     │
  │            ┌──────────────┐                  │
  │            │   REJECTED   │ ← .catch(error)  │
  │            │   (gagal)    │                  │
  │            └──────────────┘                  │
  │                                              │
  │  Setelah settled (fulfilled/rejected),       │
  │  state TIDAK BISA berubah lagi.              │
  └──────────────────────────────────────────────┘

Membuat dan Menggunakan Promise

// Membuat Promise
const myPromise = new Promise((resolve, reject) => {
  // Operasi asynchronous di sini
  const success = true;

  setTimeout(() => {
    if (success) {
      resolve('Data berhasil diambil!');  // Fulfill
    } else {
      reject(new Error('Gagal mengambil data'));  // Reject
    }
  }, 1000);
});

// Menggunakan Promise
myPromise
  .then((result) => {
    console.log('Sukses:', result);
    return result.toUpperCase();
  })
  .then((upper) => {
    console.log('Modified:', upper);
  })
  .catch((error) => {
    console.error('Error:', error.message);
  })
  .finally(() => {
    console.log('Selesai (baik sukses maupun gagal)');
  });

Promise.resolve() & Promise.reject()

// Membuat promise yang langsung resolve
const resolved = Promise.resolve(42);
resolved.then(val => console.log(val)); // 42

// Membuat promise yang langsung reject
const rejected = Promise.reject(new Error('Oops'));
rejected.catch(err => console.log(err.message)); // 'Oops'

// Promise chaining
function getUser(id) {
  return new Promise((resolve) => {
    setTimeout(() => resolve({ id, name: 'Budi' }), 500);
  });
}

function getPosts(userId) {
  return new Promise((resolve) => {
    setTimeout(() => resolve([
      { id: 1, title: 'Post 1' },
      { id: 2, title: 'Post 2' }
    ]), 500);
  });
}

// Clean promise chain (tanpa nesting!)
getUser(1)
  .then(user => {
    console.log('User:', user.name);
    return getPosts(user.id);
  })
  .then(posts => {
    console.log('Posts:', posts.length);
    return posts[0];
  })
  .then(post => {
    console.log('First post:', post.title);
  })
  .catch(err => {
    console.error('Error:', err.message);
  });

Promise dari Callback-based API

// Wrapping callback API ke Promise
function readFileAsync(path) {
  return new Promise((resolve, reject) => {
    readFile(path, (err, data) => {
      if (err) reject(err);
      else resolve(data);
    });
  });
}

// Menggunakan util.promisify (Node.js)
const { promisify } = require('util');
const readFileAsync = promisify(fs.readFile);

// Menggunakan Promise dengan DOM events
function waitForClick(element) {
  return new Promise((resolve) => {
    element.addEventListener('click', (event) => {
      resolve(event);
    }, { once: true });  // Hapus listener setelah sekali
  });
}

waitForClick(document.getElementById('btn')).then(event => {
  console.log('Button diklik!');
});

4. Async/Await Syntax

Async/Await adalah sintaksis yang diperkenalkan di ES2017 yang memungkinkan menulis kode asynchronous yang terlihat seperti synchronous. Ini adalah "syntactic sugar" di atas Promises — lebih mudah dibaca dan ditulis.

Async Function

// Fungsi yang dideklarasikan dengan 'async' selalu mengembalikan Promise
async function greet() {
  return 'Halo Dunia!';
  // Sama dengan: return Promise.resolve('Halo Dunia!');
}

greet().then(msg => console.log(msg)); // 'Halo Dunia!'

// Async function expression
const getData = async function() {
  return { data: 'test' };
};

// Async arrow function
const fetchUser = async (id) => {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
};

// Async method dalam class
class ApiService {
  async getUsers() {
    const res = await fetch('/api/users');
    return res.json();
  }

  async createUser(data) {
    const res = await fetch('/api/users', {
      method: 'POST',
      body: JSON.stringify(data)
    });
    return res.json();
  }
}

Await Keyword

// 'await' hanya bisa digunakan DI DALAM async function
// await menghentikan eksekusi sampai Promise settle

async function main() {
  console.log('Mulai');

  // Kode berhenti di sini sampai promise resolve
  const user = await getUser(1);
  console.log('User:', user.name);

  // Lalu lanjut ke sini
  const posts = await getPosts(user.id);
  console.log('Posts:', posts.length);

  console.log('Selesai');
}

main();
// Output:
// Mulai
// (tunggu 500ms)
// User: Budi
// (tunggu 500ms)
// Posts: 2
// Selesai

// Await bisa digunakan dengan nilai apapun
async function examples() {
  // Await Promise
  const data = await fetch('/api/data');

  // Await nilai biasa (langsung resolve)
  const num = await 42;  // Sama dengan Promise.resolve(42)

  // Await Promise.all
  const results = await Promise.all([
    fetch('/api/a'),
    fetch('/api/b')
  ]);
}

Perbandingan: Callback vs Promise vs Async/Await

// ═══ CALLBACK ═══
getUser(1, (err, user) => {
  if (err) return handleError(err);
  getPosts(user.id, (err, posts) => {
    if (err) return handleError(err);
    getComments(posts[0].id, (err, comments) => {
      if (err) return handleError(err);
      console.log(comments);
    });
  });
});

// ═══ PROMISE CHAINING ═══
getUser(1)
  .then(user => getPosts(user.id))
  .then(posts => getComments(posts[0].id))
  .then(comments => console.log(comments))
  .catch(handleError);

// ═══ ASYNC/AWAIT ═══
async function loadComments() {
  try {
    const user = await getUser(1);
    const posts = await getPosts(user.id);
    const comments = await getComments(posts[0].id);
    console.log(comments);
  } catch (err) {
    handleError(err);
  }
}

// Async/Await: Paling mudah dibaca, seperti synchronous code!
// Dan error handling dengan try/catch yang sudah familiar.

5. Error Handling

Error handling yang baik sangat penting dalam async programming. Async/await memungkinkan menggunakan try/catch yang familiar, tetapi ada beberapa pola yang perlu diketahui.

Try/Catch dengan Async/Await

// Pola dasar try/catch
async function fetchUserData(userId) {
  try {
    const response = await fetch(`/api/users/${userId}`);

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }

    const data = await response.json();
    return data;

  } catch (error) {
    console.error('Gagal mengambil data user:', error.message);
    throw error;  // Re-throw agar caller tahu ada error
  }
}

// Menggunakan dengan .catch() sebagai alternatif
async function fetchData() {
  const data = await fetchUserData(1).catch(err => {
    console.error('Handled:', err.message);
    return null;  // Fallback value
  });

  if (data) {
    console.log('Data:', data);
  }
}

Finally Block

// finally: selalu dijalankan, baik sukses maupun gagal
async function processData() {
  const loader = showLoader();

  try {
    const data = await fetch('/api/data');
    const result = await data.json();
    displayResult(result);
    return result;

  } catch (error) {
    showError(error.message);
    throw error;

  } finally {
    hideLoader(loader);  // Selalu dijalankan!
    // Berguna untuk cleanup: tutup connection, hapus loading, dll
  }
}

Error Handling di Promise Chain

// .catch() di akhir chain menangkap SEMUA error
fetch('/api/users/1')
  .then(res => {
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return res.json();
  })
  .then(user => fetch(`/api/posts?userId=${user.id}`))
  .then(res => res.json())
  .then(posts => console.log(posts))
  .catch(err => {
    // Error dari MANA SAJA dalam chain ditangkap di sini
    console.error('Error:', err.message);
  })
  .finally(() => {
    console.log('Done');
  });

// .catch() di tengah chain — recovery
fetch('/api/primary-data')
  .then(res => res.json())
  .catch(err => {
    console.warn('Primary failed, trying fallback');
    return fetch('/api/fallback-data').then(r => r.json());
  })
  .then(data => {
    console.log('Got data:', data);
  });

// Specific error handling
async function apiCall() {
  try {
    const data = await riskyOperation();
    return data;
  } catch (error) {
    if (error instanceof TypeError) {
      // Network error
      console.error('Network issue:', error.message);
    } else if (error.name === 'AbortError') {
      // Request aborted
      console.warn('Request was aborted');
    } else if (error.status === 404) {
      // Not found
      console.error('Resource not found');
    } else if (error.status === 401) {
      // Unauthorized
      await refreshToken();
      return riskyOperation();  // Retry
    } else {
      // Unknown error
      console.error('Unknown error:', error);
      throw error;  // Re-throw
    }
  }
}

Custom Error Classes

// Custom error untuk API
class ApiError extends Error {
  constructor(message, status, endpoint) {
    super(message);
    this.name = 'ApiError';
    this.status = status;
    this.endpoint = endpoint;
    this.timestamp = new Date().toISOString();
  }
}

class NetworkError extends Error {
  constructor(message) {
    super(message);
    this.name = 'NetworkError';
    this.retryable = true;
  }
}

class TimeoutError extends Error {
  constructor(ms) {
    super(`Request timed out after ${ms}ms`);
    this.name = 'TimeoutError';
    this.retryable = true;
  }
}

// Menggunakan custom errors
async function apiFetch(url, options = {}) {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), options.timeout || 10000);

  try {
    const response = await fetch(url, {
      ...options,
      signal: controller.signal
    });

    clearTimeout(timeout);

    if (!response.ok) {
      throw new ApiError(
        `API error: ${response.statusText}`,
        response.status,
        url
      );
    }

    return await response.json();

  } catch (error) {
    clearTimeout(timeout);

    if (error.name === 'AbortError') {
      throw new TimeoutError(options.timeout || 10000);
    }
    if (error instanceof TypeError && error.message.includes('fetch')) {
      throw new NetworkError('Network connection failed');
    }
    throw error;
  }
}

6. Promise.all, race, allSettled, any

JavaScript menyediakan beberapa static methods pada Promise untuk mengelola multiple promises secara bersamaan.

Promise.all()

// Promise.all: Jalankan paralel, gagal jika SATU gagal
async function loadDashboard() {
  const [users, posts, stats] = await Promise.all([
    fetch('/api/users').then(r => r.json()),
    fetch('/api/posts').then(r => r.json()),
    fetch('/api/stats').then(r => r.json())
  ]);

  console.log(users.length, 'users');
  console.log(posts.length, 'posts');
  console.log(stats);
}

// ⚠️ Jika salah satu gagal, SEMUA gagal
try {
  await Promise.all([
    fetch('/api/a'),         // Berhasil
    fetch('/api/b'),         // GAGAL!
    fetch('/api/c')          // Tidak dijalankan
  ]);
} catch (error) {
  console.error('Satu request gagal:', error.message);
}

Promise.allSettled()

// Promise.allSettled: Tunggu SEMUA selesai (sukses atau gagal)
// Tidak pernah reject — selalu resolve dengan array of results
async function fetchAllData() {
  const results = await Promise.allSettled([
    fetch('/api/users').then(r => r.json()),
    fetch('/api/posts').then(r => r.json()),
    fetch('/api/might-fail').then(r => r.json())
  ]);

  results.forEach((result, index) => {
    if (result.status === 'fulfilled') {
      console.log(`Request ${index}: ✅`, result.value);
    } else {
      console.log(`Request ${index}: ❌`, result.reason.message);
    }
  });

  // Filter hanya yang berhasil
  const successes = results
    .filter(r => r.status === 'fulfilled')
    .map(r => r.value);

  return successes;
}

// Contoh real: batch processing
async function processBatch(items) {
  const results = await Promise.allSettled(
    items.map(item => processItem(item))
  );

  const successful = results.filter(r => r.status === 'fulfilled');
  const failed = results.filter(r => r.status === 'rejected');

  console.log(`${successful.length}/${items.length} berhasil`);
  if (failed.length > 0) {
    console.warn('Gagal:', failed.map(r => r.reason.message));
  }
}

Promise.race()

// Promise.race: Selesai saat Promise PERTAMA settle
// (baik resolve maupun reject)

// Timeout pattern
function fetchWithTimeout(url, ms = 5000) {
  const fetchPromise = fetch(url);
  const timeoutPromise = new Promise((_, reject) => {
    setTimeout(() => reject(new Error('Timeout!')), ms);
  });

  return Promise.race([fetchPromise, timeoutPromise]);
}

// Menggunakan
try {
  const response = await fetchWithTimeout('/api/data', 3000);
  const data = await response.json();
} catch (err) {
  if (err.message === 'Timeout!') {
    console.error('Request timeout setelah 3 detik');
  }
}

// Fastest response dari multiple sources
async function getFromFastestSource() {
  const result = await Promise.race([
    fetch('/api/primary').then(r => r.json()),
    fetch('/api/backup').then(r => r.json()),
    fetch('/api/cdn').then(r => r.json())
  ]);
  return result;  // Dari source yang paling cepat
}

Promise.any()

// Promise.any: Resolve dari Promise pertama yang BERHASIL
// Hanya reject jika SEMUA promise reject

async function fetchFromMirrors(urls) {
  try {
    const data = await Promise.any(
      urls.map(url => fetch(url).then(r => {
        if (!r.ok) throw new Error(`HTTP ${r.status}`);
        return r.json();
      }))
    );
    return data;
  } catch (error) {
    // AggregateError: semua mirror gagal
    console.error('Semua mirror gagal:', error.errors);
  }
}

// Contoh: CDN fallback
const mirrors = [
  'https://cdn1.example.com/data.json',
  'https://cdn2.example.com/data.json',
  'https://cdn3.example.com/data.json'
];

const data = await fetchFromMirrors(mirrors);

// Perbandingan:
// Promise.all  → Gagal jika SATU gagal
// Promise.race → Selesai yang PERTAMA (sukses atau gagal)
// Promise.any  → Selesai yang PERTAMA SUKSES
// Promise.allSettled → Tunggu SEMUA selesai

Perbandingan Promise Methods

Method Resolves When Rejects When Use Case
Promise.allSemua fulfilledSatu saja rejectedBatch requests yang saling bergantung
Promise.allSettledSemua selesaiTidak pernahBatch requests independen
Promise.raceYang pertama settleYang pertama rejectTimeout, fastest source
Promise.anyYang pertama fulfilledSemua rejectedCDN fallback, mirror

7. Fetch API dengan Async/Await

Fetch API adalah cara modern untuk melakukan HTTP requests di browser dan Node.js (v18+). Dikombinasikan dengan async/await, kode menjadi sangat clean dan readable.

Dasar Fetch dengan Async/Await

// GET request
async function getUsers() {
  const response = await fetch('https://jsonplaceholder.typicode.com/users');

  if (!response.ok) {
    throw new Error(`HTTP error! Status: ${response.status}`);
  }

  const users = await response.json();
  return users;
}

// POST request
async function createUser(userData) {
  const response = await fetch('https://jsonplaceholder.typicode.com/users', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(userData),
  });

  if (!response.ok) {
    throw new Error(`HTTP error! Status: ${response.status}`);
  }

  const newUser = await response.json();
  return newUser;
}

// PUT request
async function updateUser(id, data) {
  const response = await fetch(`/api/users/${id}`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
  });
  return response.json();
}

// DELETE request
async function deleteUser(id) {
  const response = await fetch(`/api/users/${id}`, {
    method: 'DELETE',
  });
  return response.ok;
}

API Client Class

// Reusable API client dengan async/await
class ApiClient {
  constructor(baseURL) {
    this.baseURL = baseURL;
    this.defaultHeaders = {
      'Content-Type': 'application/json',
    };
  }

  setToken(token) {
    this.defaultHeaders['Authorization'] = `Bearer ${token}`;
  }

  async request(endpoint, options = {}) {
    const url = `${this.baseURL}${endpoint}`;
    const config = {
      headers: { ...this.defaultHeaders, ...options.headers },
      ...options,
    };

    try {
      const response = await fetch(url, config);

      // Auto-refresh token on 401
      if (response.status === 401) {
        await this.refreshToken();
        config.headers['Authorization'] = `Bearer ${this.token}`;
        const retryResponse = await fetch(url, config);
        return retryResponse.json();
      }

      if (!response.ok) {
        const error = await response.json().catch(() => ({}));
        throw new ApiError(error.message || response.statusText, response.status);
      }

      // Handle 204 No Content
      if (response.status === 204) return null;

      return response.json();

    } catch (error) {
      if (error instanceof TypeError) {
        throw new NetworkError('Koneksi gagal');
      }
      throw error;
    }
  }

  get(endpoint) {
    return this.request(endpoint, { method: 'GET' });
  }

  post(endpoint, data) {
    return this.request(endpoint, {
      method: 'POST',
      body: JSON.stringify(data),
    });
  }

  put(endpoint, data) {
    return this.request(endpoint, {
      method: 'PUT',
      body: JSON.stringify(data),
    });
  }

  delete(endpoint) {
    return this.request(endpoint, { method: 'DELETE' });
  }
}

// Penggunaan
const api = new ApiClient('https://api.example.com');
api.setToken('my-jwt-token');

const users = await api.get('/users');
const newUser = await api.post('/users', { name: 'Budi' });
await api.put(`/users/${newUser.id}`, { name: 'Budi Santoso' });
await api.delete(`/users/${newUser.id}`);

Fetch dengan Query Parameters

// Query params helper
async function fetchWithParams(baseUrl, params = {}) {
  const url = new URL(baseUrl);
  Object.entries(params).forEach(([key, value]) => {
    if (value !== undefined && value !== null) {
      url.searchParams.append(key, value);
    }
  });

  const response = await fetch(url);
  return response.json();
}

// Penggunaan
const products = await fetchWithParams('/api/products', {
  category: 'electronics',
  minPrice: 100000,
  maxPrice: 5000000,
  sort: 'price-asc',
  page: 1,
  limit: 20
});

// URL: /api/products?category=electronics&minPrice=100000&...

8. Pola Async Modern

Retry Pattern

// Retry dengan exponential backoff
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
  let lastError;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url, options);

      if (response.ok) return response.json();

      // Jangan retry untuk error 4xx (client error)
      if (response.status >= 400 && response.status < 500) {
        throw new Error(`Client error: ${response.status}`);
      }

      throw new Error(`Server error: ${response.status}`);

    } catch (error) {
      lastError = error;

      if (attempt < maxRetries) {
        // Exponential backoff: 1s, 2s, 4s
        const delay = Math.pow(2, attempt) * 1000;
        console.warn(`Attempt ${attempt + 1} failed, retrying in ${delay}ms...`);
        await new Promise(r => setTimeout(r, delay));
      }
    }
  }

  throw lastError;  // Semua retry gagal
}

// Penggunaan
const data = await fetchWithRetry('/api/data', {}, 3);

Debounce Async

// Debounce untuk search input
function debounceAsync(fn, delay = 300) {
  let timeoutId;
  let pendingReject;

  return (...args) => {
    return new Promise((resolve, reject) => {
      // Cancel previous pending request
      if (pendingReject) {
        pendingReject(new Error('Cancelled'));
      }
      pendingReject = reject;

      clearTimeout(timeoutId);
      timeoutId = setTimeout(async () => {
        try {
          const result = await fn(...args);
          resolve(result);
        } catch (err) {
          reject(err);
        }
      }, delay);
    });
  };
}

// Penggunaan
const searchAPI = debounceAsync(async (query) => {
  const response = await fetch(`/api/search?q=${query}`);
  return response.json();
}, 300);

searchInput.addEventListener('input', async (e) => {
  try {
    const results = await searchAPI(e.target.value);
    displayResults(results);
  } catch (err) {
    if (err.message !== 'Cancelled') {
      console.error('Search error:', err);
    }
  }
});

Singleton Lock Pattern

// Prevent multiple concurrent requests ke endpoint yang sama
class RequestLock {
  constructor() {
    this.locks = new Map();
  }

  async execute(key, asyncFn) {
    if (this.locks.has(key)) {
      return this.locks.get(key);
    }

    const promise = asyncFn().finally(() => {
      this.locks.delete(key);
    });

    this.locks.set(key, promise);
    return promise;
  }
}

const locks = new RequestLock();

async function getUserProfile(userId) {
  return locks.execute(`user-${userId}`, async () => {
    const response = await fetch(`/api/users/${userId}`);
    return response.json();
  });
}

// Kedua call ini akan menggunakan request yang sama
const [profile1, profile2] = await Promise.all([
  getUserProfile(1),  // Membuat request
  getUserProfile(1)   // Menggunakan request yang sama!
]);

Async Iterator / Generator

// Async Generator untuk paginated API
async function* fetchAllPages(baseUrl) {
  let page = 1;
  let hasMore = true;

  while (hasMore) {
    const response = await fetch(`${baseUrl}?page=${page}&limit=100`);
    const data = await response.json();

    yield data.items;  // Yield batch of items

    hasMore = data.hasNextPage;
    page++;
  }
}

// Menggunakan for await...of
async function processAllItems() {
  for await (const batch of fetchAllPages('/api/items')) {
    for (const item of batch) {
      await processItem(item);
    }
  }
}

// Async Iterator untuk streaming data
async function* streamData(url) {
  const response = await fetch(url);
  const reader = response.body.getReader();
  const decoder = new TextDecoder();

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    yield decoder.decode(value, { stream: true });
  }
}

// Menggunakan
for await (const chunk of streamData('/api/large-data')) {
  processChunk(chunk);
}

Top-Level Await (ES2022)

// Top-level await: await di luar async function
// Hanya bekerja di ES Modules (<script type="module">)

// config.js
const response = await fetch('/api/config');
export const config = await response.json();

// main.js
import { config } from './config.js';
console.log(config.apiKey);  // Langsung tersedia!

// Contoh: Dynamic import
const module = await import('./heavy-module.js');
module.init();

// Contoh: Resource initialization
const db = await initializeDatabase();
export default db;

9. Kesalahan Umum

⚠️ Kesalahan yang Harus Dihindari

1. Lupa Await

// ❌ SALAH: Lupa await
async function bad() {
  const response = fetch('/api/data');  // Promise, bukan response!
  console.log(response);  // Promise { <pending> }
}

// ✅ BENAR
async function good() {
  const response = await fetch('/api/data');  // Tunggu selesai
  console.log(response);  // Response object
}

2. Await di Loop (Sequential vs Parallel)

// ❌ SEQUENTIAL: Satu per satu (lambat!)
async function bad(urls) {
  const results = [];
  for (const url of urls) {
    const response = await fetch(url);  // Tunggu selesai dulu!
    const data = await response.json();
    results.push(data);
  }
  return results;  // Total = semua durasi ditambahkan
}

// ✅ PARALLEL: Semua sekaligus (cepat!)
async function good(urls) {
  const promises = urls.map(async (url) => {
    const response = await fetch(url);
    return response.json();
  });
  return Promise.all(promises);  // Total = durasi terpanjang
}

// ✅ Batched parallel (jika terlalu banyak request)
async function batched(urls, batchSize = 5) {
  const results = [];
  for (let i = 0; i < urls.length; i += batchSize) {
    const batch = urls.slice(i, i + batchSize);
    const batchResults = await Promise.all(
      batch.map(url => fetch(url).then(r => r.json()))
    );
    results.push(...batchResults);
  }
  return results;
}

3. Error Handling yang Hilang

// ❌ SALAH: Tidak ada error handling
async function fetchData() {
  const response = await fetch('/api/data');  // Bisa throw!
  return response.json();
}
// Jika error, unhandled promise rejection!

// ✅ BENAR: Selalu handle errors
async function fetchData() {
  try {
    const response = await fetch('/api/data');
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return response.json();
  } catch (error) {
    console.error('Fetch failed:', error);
    return null;  // Fallback
  }
}

4. Membuat Async yang Tidak Perlu

// ❌ Tidak perlu async untuk operasi sync
async function getConfig() {
  return localStorage.getItem('config');  // Sync operation!
}

// ✅ Langsung return
function getConfig() {
  return localStorage.getItem('config');
}

// ❌ Async di forEach tidak bekerja seperti yang diharapkan
async function bad() {
  [1, 2, 3].forEach(async (num) => {
    await delay(1000);
    console.log(num);  // Semua jalan paralel!
  });
  console.log('Done');  // Ini jalan SEBELUM forEach selesai!
}

// ✅ Gunakan for...of atau Promise.all
async function good() {
  for (const num of [1, 2, 3]) {
    await delay(1000);
    console.log(num);  // Berurutan
  }
  console.log('Done');  // Setelah semua selesai
}

5. Memory Leak dengan Async

// ❌ Potensi memory leak: event listener yang tidak di-cleanup
async function loadData() {
  document.getElementById('btn').addEventListener('click', async () => {
    const data = await fetch('/api/data');
    // Jika komponen sudah unmounted, setState akan error
    this.setState(await data.json());
  });
}

// ✅ Gunakan AbortController
const controller = new AbortController();

async function loadData() {
  try {
    const response = await fetch('/api/data', {
      signal: controller.signal
    });
    return await response.json();
  } catch (err) {
    if (err.name === 'AbortError') {
      console.log('Request dibatalkan');
      return null;
    }
    throw err;
  }
}

// Cancel saat component unmount
window.addEventListener('beforeunload', () => controller.abort());

10. Quiz Pemahaman

Uji pemahamanmu tentang JavaScript Async/Await:

Pertanyaan 1: Apa output dari kode berikut?
console.log('A'); setTimeout(() => console.log('B'), 0); Promise.resolve().then(() => console.log('C')); console.log('D');

a) A, D, B, C
b) A, D, C, B
c) A, B, C, D
d) A, C, D, B

Pertanyaan 2: async function selalu mengembalikan apa?

a) Undefined
b) Callback function
c) Promise
d) Generator

Pertanyaan 3: Apa perbedaan antara Promise.all dan Promise.allSettled?

a) Tidak ada perbedaan
b) Promise.all gagal jika SATU reject, Promise.allSettled tunggu semua selesai
c) Promise.allSettled lebih cepat
d) Promise.all hanya untuk GET request

Pertanyaan 4: Pola apa yang digunakan untuk membatalkan fetch request?

a) fetch.cancel()
b) AbortController
c) Promise.reject()
d) clearTimeout()

Pertanyaan 5: Bagaimana cara menjalankan 3 fetch request secara PARALEL?

a) await fetch(a); await fetch(b); await fetch(c);
b) await Promise.all([fetch(a), fetch(b), fetch(c)])
c) await Promise.race([fetch(a), fetch(b), fetch(c)])
d) fetch(a).then(fetch(b)).then(fetch(c))
🔍 Zoom
100%
🎨 Tema