Skillversitas

Async/Await

async/await is syntactic sugar over Promises that makes asynchronous code look like synchronous code. A function declared with async always returns a Promise. await can only be used inside an async function.

Code Example

Async/Await Basics

// An async function — always returns a Promise
async function fetchData() {
  const response = await fetch('https://api.university.edu/student');
  const data = await response.json();
  return data;
}

// Using it
fetchData().then(data => console.log(data));

Error Handling with try...catch

async function fetchStudent(studentId) {
  try {
    const response = await fetch(`/api/student/${studentId}`);

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

    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Failed to fetch data:', error.message);
    // You can re-throw or return a default
    return null;
  }
}

Await with a Promise

async function processData() {
  // await on a Promise — waits until it resolves
  const data = await new Promise((resolve) => {
    setTimeout(() => resolve('Data from Promise'), 1000);
  });

  console.log(data); // "Data from Promise"
  return data;
}

Code Explanation

Parallel vs Sequential

// SEQUENTIAL — one by one (slow)
async function sequential() {
  console.time('sequential');

  const student = await fetch('/api/student').then(r => r.json());
  const dsn = await fetch('/api/lecturer').then(r => r.json());
  const mk = await fetch('/api/course').then(r => r.json());

  console.timeEnd('sequential'); // ~3 seconds if each takes 1 second
  return { student, dsn, mk };
}

// PARALLEL — at the same time (fast)
async function parallel() {
  console.time('parallel');

  const [student, dsn, mk] = await Promise.all([
    fetch('/api/student').then(r => r.json()),
    fetch('/api/lecturer').then(r => r.json()),
    fetch('/api/course').then(r => r.json())
  ]);

  console.timeEnd('parallel'); // ~1 second because they run concurrently
  return { student, dsn, mk };
}

Top-Level Await

// In a module (type="module" on the script tag)
// Await can be used directly at the top level without wrapping in an async function

const response = await fetch('/api/student');
const data = await response.json();
console.log(data);

// Useful for initial application setup
const config = await fetch('/api/config').then(r => r.json());
const user = await fetch('/api/user/me').then(r => r.json());

renderApp(config, user);

For...of with Await

// Sequential processing with a loop
async function processFiles(fileList) {
  for (const file of fileList) {
    try {
      const response = await fetch('/api/upload', {
        method: 'POST',
        body: file
      });
      console.log(`${file.name} uploaded successfully`);
    } catch (error) {
      console.error(`Failed to upload ${file.name}:`, error);
    }
  }
}

Real-World Examples

Sequential API Calls: Login → Profile → Courses

async function loginDanLoadDashboard(email, password) {
  try {
    // Step 1: Login
    console.log('Sedang login...');
    const loginRes = await fetch('/api/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password })
    });

    if (!loginRes.ok) {
      throw new Error('Email or password incorrect');
    }

    const { token } = await loginRes.json();
    localStorage.setItem('authToken', token);

    // Step 2: Fetch the profile
    console.log('Loading profile...');
    const profileRes = await fetch('/api/user/profile', {
      headers: { 'Authorization': `Bearer ${token}` }
    });

    if (!profileRes.ok) throw new Error('Failed to load profile');

    const profile = await profileRes.json();
    renderProfile(profile);

    // Step 3: Fetch the courses
    console.log('Loading course...');
    const coursesRes = await fetch(`/api/courses?student=${profile.studentId}`, {
      headers: { 'Authorization': `Bearer ${token}` }
    });

    if (!coursesRes.ok) throw new Error('Failed to load courses');

    const courses = await coursesRes.json();
    renderCourses(courses);

    // Everything succeeded
    showNotification('Welcome, ' + profile.name + '!', 'success');

  } catch (error) {
    showNotification(error.message, 'error');
    // Redirect to login if the session expired
    if (error.message.includes('401')) {
      window.location.href = '/login';
    }
  }
}

Parallel Fetch for a Dashboard

async function loadDashboardData() {
  const token = localStorage.getItem('authToken');
  const headers = { 'Authorization': `Bearer ${token}` };

  // Fetch all data in parallel
  const [profileRes, statsRes, jadwalRes, notifRes] = await Promise.all([
    fetch('/api/user/profile', { headers }),
    fetch('/api/statistics/student', { headers }),
    fetch('/api/schedule/this-week', { headers }),
    fetch('/api/notifikasi?unreadOnly=true', { headers })
  ]);

  // Parse all the JSON in parallel
  const [profile, stats, schedule, notifikasi] = await Promise.all([
    profileRes.json(),
    statsRes.json(),
    jadwalRes.json(),
    notifRes.json()
  ]);

  // Render all components
  renderProfile(profile);
  renderStatistics(stats);
  renderJadwal(schedule);
  renderNotifikasi(notifikasi);
}

Robust Error Handling

async function fetchDataWithFallback() {
  try {
    // Try the primary server
    const response = await fetch('https://api.university.edu/data');
    if (!response.ok) throw new Error('Main server error');
    return await response.json();
  } catch (primaryError) {
    console.warn('Primary server failed, trying mirror...');

    try {
      // Fall back to the mirror
      const response = await fetch('https://api-mirror.university.edu/data');
      if (!response.ok) throw new Error('Mirror error');
      return await response.json();
    } catch (mirrorError) {
      console.warn('Mirror also failed, using cache...');

      // Fall back to the cache
      const cached = localStorage.getItem('cachedData');
      if (cached) return JSON.parse(cached);

      throw new Error('All data sources failed');
    }
  }
}

Upload with Progress (Using ReadableStream)

async function uploadWithProgress(file, onProgress) {
  const formData = new FormData();
  formData.append('file', file);

  const response = await fetch('/api/upload', {
    method: 'POST',
    body: formData,
    // Note: real-time progress tracking requires XMLHttpRequest
    // or the Streaming API for fetch
  });

  if (!response.ok) throw new Error('Upload failed');
  return response.json();
}

Async vs Promise

// With Promise chains
function loadData() {
  return fetch('/api/data')
    .then(r => r.json())
    .then(data => {
      return fetch(`/api/detail/${data.id}`);
    })
    .then(r => r.json())
    .then(detail => {
      render(detail);
    })
    .catch(err => {
      console.error(err);
    });
}

// With async/await — easier to read
async function loadData() {
  try {
    const dataRes = await fetch('/api/data');
    const data = await dataRes.json();

    const detailRes = await fetch(`/api/detail/${data.id}`);
    const detail = await detailRes.json();

    render(detail);
  } catch (err) {
    console.error(err);
  }
}

Conclusion

async/await is the cleanest way to write asynchronous JavaScript. Use try/catch for error handling, Promise.all() for parallel operations, and top-level await for module initialization. Sequential awaits are right for dependent operations (login → profile → data), while parallel execution with Promise.all() is for independent operations.