-
-
Notifications
You must be signed in to change notification settings - Fork 2
JavaScript Async Programming
- Introduction
- Understanding Asynchronous JavaScript
- Callbacks
- Promises
- Async/Await
- Fetch API
- Advanced Patterns
- Real-World Examples
- Best Practices
Asynchronous JavaScript is essential for building responsive web applications. This guide covers callbacks, promises, async/await, and the Fetch API - the core tools for handling asynchronous operations in JavaScript.
JavaScript is single-threaded, meaning it can only do one thing at a time. Asynchronous programming allows you to execute long-running operations without blocking the main thread.
// Synchronous code - executes in order
console.log("1. Start");
console.log("2. Processing");
console.log("3. End");
// Asynchronous code - doesn't wait
console.log("1. Start");
setTimeout(() => console.log("2. Timeout!"), 1000);
console.log("3. End");
// Output: 1, 3, 2 (order may vary)Callbacks are functions passed as arguments to other functions, executed after an operation completes.
// Simple callback
function greet(name, callback) {
const message = "Hello, " + name;
callback(message);
}
greet("Alice", (message) => {
console.log(message); // "Hello, Alice"
});
// Callback with error handling
function fetchData(callback) {
// Simulate async operation
setTimeout(() => {
const data = { name: "Alice", age: 25 };
const error = null;
callback(error, data);
}, 1000);
}
fetchData((error, data) => {
if (error) {
console.error("Error:", error);
} else {
console.log("Data:", data);
}
});
// Callback hell - avoid this!
getData((error, data) => {
if (error) return handleError(error);
processData(data, (error, result) => {
if (error) return handleError(error);
saveResult(result, (error, saved) => {
if (error) return handleError(error);
console.log("Complete!");
});
});
});Promises provide a cleaner way to handle asynchronous operations compared to callbacks.
// Create a new Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
if (success) {
resolve("Operation successful!");
} else {
reject("Operation failed!");
}
});
// Using the promise
myPromise
.then((result) => console.log(result))
.catch((error) => console.error(error));
// Promise with setTimeout
function delay(ms) {
return new Promise((resolve) => {
setTimeout(() => resolve("Done!"), ms);
});
}
delay(1000).then((result) => console.log(result));A promise can be in one of three states:
// pending - initial state, neither fulfilled nor rejected
const pendingPromise = new Promise((resolve, reject) => {});
// fulfilled - operation completed successfully
const fulfilledPromise = Promise.resolve("Success!");
// rejected - operation failed
const rejectedPromise = Promise.reject("Error!");
// Check state (can't directly check, but can handle)
const promise = new Promise((resolve, reject) => {
setTimeout(() => Math.random() > 0.5 ? resolve("OK") : reject("Fail"), 1000);
});
promise
.then((result) => console.log("Fulfilled:", result))
.catch((error) => console.log("Rejected:", error))
.finally(() => console.log("Promise settled"));// Promise.resolve() - create resolved promise
const resolved = Promise.resolve(42);
// Promise.reject() - create rejected promise
const rejected = Promise.reject("Error");
// Promise.all() - wait for all promises
const results = Promise.all([
Promise.resolve(1),
Promise.resolve(2),
Promise.resolve(3)
]);
results.then((values) => console.log(values)); // [1, 2, 3]
// Promise.race() - return first settled promise
const first = Promise.race([
new Promise(r => setTimeout(() => r("fast"), 100)),
new Promise(r => setTimeout(() => r("slow"), 200))
]);
first.then(console.log); // "fast"
// Promise.allSettled() - wait for all to settle
const allSettled = Promise.allSettled([
Promise.resolve(1),
Promise.reject("error"),
Promise.resolve(3)
]);
allSettled.then(console.log);
// [{status: "fulfilled", value: 1}, {status: "rejected", reason: "error"}, ...]
// Promise.any() - first fulfilled (ES2021)
const any = Promise.any([
Promise.reject("error"),
Promise.resolve("success"),
Promise.resolve("also success")
]);
any.then(console.log); // "success"// Chain promises
fetchUser(1)
.then((user) => fetchPosts(user.id))
.then((posts) => fetchComments(posts[0].id))
.then((comments) => console.log(comments))
.catch((error) => console.error("Error:", error));
// Return values in chain
Promise.resolve(1)
.then((x) => x + 1) // 2
.then((x) => x * 2) // 4
.then((x) => x.toString()) // "4"
.then(console.log);
// Returning promises in chain
function fetchUser(id) {
return fetch(`/api/users/${id}`).then(r => r.json());
}
fetchUser(1)
.then((user) => {
// Can return another promise here
return fetch(`/api/posts?user=${user.id}`).then(r => r.json());
})
.then((posts) => console.log(posts));Async/await provides a cleaner syntax for working with promises.
// async function declaration
async function fetchData() {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
return data;
}
// arrow function
const fetchData = async () => {
const response = await fetch('https://api.example.com/data');
return response.json();
};
// calling async function
fetchData()
.then((data) => console.log(data))
.catch((error) => console.error(error));
// or use IIFE
(async () => {
try {
const data = await fetchData();
console.log(data);
} catch (error) {
console.error(error);
}
})();// try/catch for error handling
async function fetchUser(id) {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const user = await response.json();
return user;
} catch (error) {
console.error("Failed to fetch user:", error);
throw error; // re-throw for caller
}
}
// Multiple try/catch blocks
async function getData() {
try {
const user = await fetchUser(1);
try {
const posts = await fetchPosts(user.id);
return { user, posts };
} catch (postError) {
return { user, posts: [] }; // Graceful degradation
}
} catch (userError) {
return { user: null, posts: [] };
}
}
// Catch with async/await
async function main() {
const user = await fetchUser(1).catch((error) => {
console.error(error);
return null;
});
}// Sequential (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 { user, posts, comments };
}
// Parallel (all at once)
async function parallel() {
const [user, posts, comments] = await Promise.all([
fetchUser(1),
fetchPosts(1),
fetchComments(1)
]);
return { user, posts, comments };
}
// When to use each:
// - Sequential: when each depends on previous result
// - Parallel: when operations are independent// Example: Fetching multiple users
const userIds = [1, 2, 3, 4, 5];
// Sequential - takes 5x time
async function fetchSequential() {
const users = [];
for (const id of userIds) {
const user = await fetchUser(id);
users.push(user);
}
return users;
}
// Parallel - takes 1x time
async function fetchParallel() {
const promises = userIds.map(id => fetchUser(id));
return Promise.all(promises);
}
// Mixed approach - fetch in batches
async function fetchInBatches(batchSize = 2) {
const results = [];
for (let i = 0; i < userIds.length; i += batchSize) {
const batch = userIds.slice(i, i + batchSize);
const batchResults = await Promise.all(batch.map(id => fetchUser(id)));
results.push(...batchResults);
}
return results;
}// GET request
fetch('https://api.example.com/users')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error(error));
// POST request
fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'Alice',
email: 'alice@example.com'
})
})
.then(response => response.json())
.then(data => console.log(data));// Complete fetch options
const options = {
method: 'GET', // GET, POST, PUT, DELETE, etc.
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token',
'Accept': 'application/json'
},
body: JSON.stringify(data), // for POST/PUT
mode: 'cors', // cors, same-origin, no-cors
credentials: 'include', // include, same-origin, omit
cache: 'no-cache', // default, no-cache, reload, force-cache
redirect: 'follow', // follow, error, manual
referrer: 'no-referrer'
};
fetch(url, options);// Response methods
async function handleResponse(response) {
// Check content type and parse accordingly
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
const data = await response.json();
return data;
}
if (contentType && contentType.includes('text/html')) {
const text = await response.text();
return text;
}
// Handle other types...
return response.blob();
}
// Response properties
async function inspectResponse(response) {
console.log('Status:', response.status); // 200
console.log('Status Text:', response.statusText); // OK
console.log('OK:', response.ok); // true if 200-299
console.log('URL:', response.url);
console.log('Type:', response.type); // basic, cors, opaque
// Headers
for (const [key, value] of response.headers) {
console.log(key, value);
}
}Wait for all promises to resolve, fail if any fail:
// Fetch multiple resources
async function loadPage() {
const [header, sidebar, content] = await Promise.all([
fetch('/api/header').then(r => r.json()),
fetch('/api/sidebar').then(r => r.json()),
fetch('/api/content').then(r => r.json())
]);
return { header, sidebar, content };
}
// Handle partial failures with allSettled first
async function loadWithPartialFailure() {
const results = await Promise.allSettled([
fetch('/api/data1').then(r => r.json()),
fetch('/api/data2').then(r => r.json())
]);
const data = results
.filter(r => r.status === 'fulfilled')
.map(r => r.value);
return data;
}Return the first promise to settle (resolve or reject):
// Timeout pattern
function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Timeout')), ms);
});
return Promise.race([promise, timeout]);
}
async function fetchWithTimeout() {
try {
const data = await withTimeout(fetch('/api/slow'), 5000);
return data;
} catch (error) {
console.error('Fetch failed or timed out');
}
}Wait for all promises to settle (never rejects):
// Get results regardless of failures
async function fetchAllUsers(userIds) {
const promises = userIds.map(id =>
fetch(`/api/users/${id}`).then(r => r.json())
);
const results = await Promise.allSettled(promises);
return results.map((result, index) => ({
id: userIds[index],
success: result.status === 'fulfilled',
data: result.value,
error: result.reason
}));
}Return first fulfilled promise (ignore rejections):
// Try multiple servers, use first success
async function fetchFromServers() {
const urls = [
'https://server1.example.com/data',
'https://server2.example.com/data',
'https://server3.example.com/data'
];
const promises = urls.map(url =>
fetch(url).then(r => r.json())
);
try {
const data = await Promise.any(promises);
return data;
} catch (error) {
console.error('All servers failed');
}
}// REST API wrapper
class APIClient {
constructor(baseURL) {
this.baseURL = baseURL;
}
async request(endpoint, options = {}) {
const url = this.baseURL + endpoint;
const config = {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
};
try {
const response = await fetch(url, config);
if (!response.ok) {
throw new APIError(response.status, await response.text());
}
return await response.json();
} catch (error) {
if (error instanceof APIError) throw error;
throw new NetworkError(error.message);
}
}
get(endpoint) {
return this.request(endpoint);
}
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' });
}
}
class APIError extends Error {
constructor(status, message) {
super(message);
this.status = status;
this.name = 'APIError';
}
}
class NetworkError extends Error {
constructor(message) {
super(message);
this.name = 'NetworkError';
}
}
// Usage
const api = new APIClient('https://api.example.com');
const users = await api.get('/users');
await api.post('/users', { name: 'Alice' });const fs = require('fs').promises;
// Read file
async function readFile(path) {
try {
const data = await fs.readFile(path, 'utf-8');
return data;
} catch (error) {
console.error('Error reading file:', error);
throw error;
}
}
// Write file
async function writeFile(path, data) {
await fs.writeFile(path, JSON.stringify(data, null, 2));
}
// Copy file
async function copyFile(source, destination) {
await fs.copyFile(source, destination);
}
// Read directory
async function listFiles(path) {
const entries = await fs.readdir(path, { withFileTypes: true });
return entries.map(entry => ({
name: entry.name,
isDirectory: entry.isDirectory(),
isFile: entry.isFile()
}));
}// Wait for event once
function once(element, eventName) {
return new Promise((resolve) => {
element.addEventListener(eventName, resolve, { once: true });
});
}
// Usage
const button = document.getElementById('myButton');
const clickEvent = await once(button, 'click');
console.log('Button clicked at', clickEvent.timestamp);
// Wait for multiple events
function waitForEvents(emitter, events) {
return new Promise((resolve) => {
const results = {};
let remaining = events.length;
events.forEach(event => {
emitter.on(event, (data) => {
results[event] = data;
remaining--;
if (remaining === 0) resolve(results);
});
});
});
}// Always handle errors
async function good() {
try {
const data = await fetchData();
return data;
} catch (error) {
console.error(error);
throw error;
}
}
// Use async/await over .then() chains
// Good
async function goodExample() {
const user = await fetchUser(id);
const posts = await fetchPosts(user.id);
return posts;
}
// Avoid
function badExample() {
return fetchUser(id)
.then(user => fetchPosts(user.id));
}
// Use Promise.all for parallel operations
async function loadData() {
const [users, posts] = await Promise.all([
fetchUsers(),
fetchPosts()
]);
return { users, posts };
}
// Avoid blocking the main thread
// Use Web Workers for heavy computation
const worker = new Worker('worker.js');
worker.postMessage({ data: 'heavy' });
worker.onmessage = (event) => console.log(result);
// Cancel async operations with AbortController
const controller = new AbortController();
const signal = controller.signal;
async function fetchWithCancel() {
try {
const response = await fetch('/api/data', { signal });
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
console.log('Fetch aborted');
} else {
throw error;
}
}
}
// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5000);// Promise basics
const promise = new Promise((resolve, reject) => {});
promise.then(onFulfilled).catch(onRejected);
// Async/await
async function fetchData() {
const data = await promise;
return data;
}
// Fetch API
fetch(url, options)
.then(r => r.json())
.then(data => console.log(data));
// Promise.all - wait for all
const results = await Promise.all([p1, p2, p3]);
// Promise.race - first to settle
const result = await Promise.race([p1, p2]);
// Promise.allSettled - all settle
const results = await Promise.allSettled([p1, p2]);
// Error handling
try {
const data = await fetchData();
} catch (error) {
console.error(error);
}