Asynchronous Programming

Asynchronous programming allows code to run without blocking the main thread, enabling multiple operations to happen concurrently. This is essential for web development where operations like API calls, file reading, or timers shouldn't freeze the user interface.

Synchronous vs Asynchronous

Synchronous (Blocking)

javascript
// Each line waits for previous to complete
console.log('First');
console.log('Second');
console.log('Third');

// Output (in order):
// First
// Second
// Third

Asynchronous (Non-blocking)

javascript
console.log('First');

setTimeout(() => {
  console.log('Second');
}, 1000);

console.log('Third');

// Output:
// First
// Third
// Second (after 1 second)

Asynchronous Patterns

1. Callbacks

The original pattern for async code in JavaScript.

javascript
// Callback example
function fetchUser(id, callback) {
  setTimeout(() => {
    const user = { id, name: 'John' };
    callback(user);
  }, 1000);
}

fetchUser(1, (user) => {
  console.log(user);
});

// Callback hell (pyramid of doom)
fetchUser(1, (user) => {
  fetchPosts(user.id, (posts) => {
    fetchComments(posts[0].id, (comments) => {
      console.log(comments);
    });
  });
});

2. Promises

A cleaner way to handle asynchronous operations.

javascript
// Creating a Promise
function fetchUser(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id) {
        resolve({ id, name: 'John' });
      } else {
        reject(new Error('Invalid ID'));
      }
    }, 1000);
  });
}

// Using a Promise
fetchUser(1)
  .then(user => console.log(user))
  .catch(error => console.error(error));

// Promise chaining
fetchUser(1)
  .then(user => fetchPosts(user.id))
  .then(posts => fetchComments(posts[0].id))
  .then(comments => console.log(comments))
  .catch(error => console.error(error));

3. Async/Await

Modern, readable syntax for working with Promises.

javascript
// Async function always returns a Promise
async function getUser(id) {
  try {
    const user = await fetchUser(id);
    const posts = await fetchPosts(user.id);
    const comments = await fetchComments(posts[0].id);
    return comments;
  } catch (error) {
    console.error(error);
  }
}

// Use it
getUser(1).then(comments => console.log(comments));

Common Async Operations

setTimeout / setInterval

javascript
// Run once after delay
const timeoutId = setTimeout(() => {
  console.log('Delayed message');
}, 1000);

// Cancel timeout
clearTimeout(timeoutId);

// Run repeatedly
const intervalId = setInterval(() => {
  console.log('Repeating message');
}, 1000);

// Cancel interval
clearInterval(intervalId);

Fetch API

javascript
// GET request
fetch('https://api.example.com/users')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error(error));

// With async/await
async function getUsers() {
  try {
    const response = await fetch('https://api.example.com/users');
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error(error);
  }
}

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

File Reading (Node.js)

javascript
const fs = require('fs').promises;

// Async file read
async function readFile() {
  try {
    const data = await fs.readFile('file.txt', 'utf8');
    console.log(data);
  } catch (error) {
    console.error(error);
  }
}

Promise Methods

Promise.all

Run multiple promises in parallel, wait for all to complete.

javascript
// All must succeed
const [users, posts, comments] = await Promise.all([
  fetchUsers(),
  fetchPosts(),
  fetchComments()
]);

// If any fails, entire operation fails
Promise.all([promise1, promise2, promise3])
  .then(results => console.log(results))
  .catch(error => console.error('One failed:', error));

Promise.allSettled

Wait for all promises, regardless of success or failure.

javascript
const results = await Promise.allSettled([
  fetchUsers(),
  fetchPosts(),
  fetchComments()
]);

results.forEach(result => {
  if (result.status === 'fulfilled') {
    console.log('Success:', result.value);
  } else {
    console.log('Failed:', result.reason);
  }
});

Promise.race

Return the first promise to settle (resolve or reject).

javascript
const result = await Promise.race([
  fetchFromAPI1(),
  fetchFromAPI2(),
  fetchFromAPI3()
]);

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

const data = await Promise.race([
  fetchData(),
  timeout
]);

Promise.any

Return the first promise to successfully resolve.

javascript
// First successful response wins
const data = await Promise.any([
  fetch('https://api1.example.com/data'),
  fetch('https://api2.example.com/data'),
  fetch('https://api3.example.com/data')
]);

// If all fail, throws AggregateError

Error Handling

Try/Catch with Async/Await

javascript
async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');

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

    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Fetch failed:', error);
    throw error; // Re-throw if needed
  } finally {
    console.log('Cleanup code runs regardless');
  }
}

Promise .catch()

javascript
fetchData()
  .then(data => processData(data))
  .then(result => console.log(result))
  .catch(error => console.error('Error:', error))
  .finally(() => console.log('Done'));

React Examples

useEffect with Async

jsx
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;

    async function loadUser() {
      try {
        setLoading(true);
        const response = await fetch(`/api/users/${userId}`);
        const data = await response.json();

        if (!cancelled) {
          setUser(data);
          setError(null);
        }
      } catch (err) {
        if (!cancelled) {
          setError(err.message);
        }
      } finally {
        if (!cancelled) {
          setLoading(false);
        }
      }
    }

    loadUser();

    // Cleanup function
    return () => {
      cancelled = true;
    };
  }, [userId]);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  return <div>{user?.name}</div>;
}

Custom Async Hook

jsx
function useAsync(asyncFunction) {
  const [status, setStatus] = useState('idle');
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);

  const execute = useCallback(async (...params) => {
    setStatus('pending');
    setData(null);
    setError(null);

    try {
      const response = await asyncFunction(...params);
      setData(response);
      setStatus('success');
    } catch (error) {
      setError(error);
      setStatus('error');
    }
  }, [asyncFunction]);

  return { execute, status, data, error };
}

// Usage
function Component() {
  const { execute, status, data, error } = useAsync(fetchUser);

  useEffect(() => {
    execute(1);
  }, [execute]);

  return status === 'pending' ? 'Loading...' : data?.name;
}

Common Patterns

Sequential Async Operations

javascript
// One after another
async function sequential() {
  const user = await fetchUser(1);
  const posts = await fetchPosts(user.id);
  const comments = await fetchComments(posts[0].id);
  return comments;
}

Parallel Async Operations

javascript
// All at once
async function parallel() {
  const [users, posts, comments] = await Promise.all([
    fetchUsers(),
    fetchPosts(),
    fetchComments()
  ]);
  return { users, posts, comments };
}

Retry Logic

javascript
async function fetchWithRetry(url, options = {}, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      return await fetch(url, options);
    } catch (error) {
      if (i === retries - 1) throw error;
      await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
    }
  }
}

Debounce Async

javascript
function debounceAsync(fn, delay) {
  let timeoutId;

  return async function(...args) {
    clearTimeout(timeoutId);

    return new Promise((resolve) => {
      timeoutId = setTimeout(async () => {
        const result = await fn.apply(this, args);
        resolve(result);
      }, delay);
    });
  };
}

const debouncedSearch = debounceAsync(searchAPI, 300);

Event Loop

Understanding how JavaScript handles async code:

javascript
console.log('1');

setTimeout(() => console.log('2'), 0);

Promise.resolve().then(() => console.log('3'));

console.log('4');

// Output: 1, 4, 3, 2
// Microtasks (Promises) run before macrotasks (setTimeout)

Best Practices

  • Always handle errors (try/catch or .catch())
  • Use async/await for better readability
  • Avoid mixing callbacks and Promises
  • Be careful with async in loops
  • Clean up async operations in React useEffect
  • Use Promise.all for parallel operations
  • Set timeouts for network requests
  • Consider loading and error states in UI
  • Use AbortController to cancel fetch requests

Common Pitfalls

javascript
// Bad: Async in forEach
items.forEach(async item => {
  await processItem(item);
});

// Good: Use for...of or Promise.all
for (const item of items) {
  await processItem(item);
}

// Or parallel
await Promise.all(items.map(item => processItem(item)));

// Bad: Not awaiting in try/catch
try {
  fetchData(); // Forgot await!
} catch (error) {
  // This won't catch the error
}

// Good
try {
  await fetchData();
} catch (error) {
  // This will catch the error
}

Learn More