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)
javascriptconsole.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)
javascriptconst 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.
javascriptconst 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).
javascriptconst 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
javascriptasync 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()
javascriptfetchData() .then(data => processData(data)) .then(result => console.log(result)) .catch(error => console.error('Error:', error)) .finally(() => console.log('Done'));
React Examples
useEffect with Async
jsxfunction 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
jsxfunction 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
javascriptasync 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
javascriptfunction 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:
javascriptconsole.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 }