JavaScript Async Programming
Asynchronous programming is one of the core features of JavaScript. It allows programs to continue executing other code while waiting for time-consuming operations (such as network requests, file I/O, timers, etc.) to complete, improving responsiveness and performance. Understanding asynchronous programming is crucial for building modern web applications.
What is Async Programming
Asynchronous programming means that when a program executes time-consuming operations, it doesn't block subsequent code execution. Instead, it continues executing other tasks and handles results through callbacks, Promises, or async/await when the operation completes.
Synchronous vs Asynchronous
// Synchronous code example
console.log("1. Start execution");
// This blocks subsequent code (not recommended in practice)
function syncDelay(ms) {
const start = Date.now();
while (Date.now() - start < ms) {
// Empty loop, blocks execution
}
}
syncDelay(2000); // Blocks for 2 seconds
console.log("2. Sync delay ended");
// Asynchronous code example
console.log("1. Start execution");
setTimeout(() => {
console.log("3. Async operation completed");
}, 2000);
console.log("2. Continue executing other code");Callback Functions
Callbacks were the earliest async programming approach, but can lead to callback hell.
Basic Callback
// Simple callback example
function fetchData(callback) {
setTimeout(() => {
const data = { id: 1, name: "John" };
callback(null, data); // First param is error, second is data
}, 1000);
}
fetchData((error, data) => {
if (error) {
console.error("Failed to fetch data:", error);
} else {
console.log("Data received:", data);
}
});Callback Hell
// Callback hell example
step1((error1, result1) => {
if (error1) {
console.error("Step 1 failed:", error1);
return;
}
step2(result1, (error2, result2) => {
if (error2) {
console.error("Step 2 failed:", error2);
return;
}
step3(result2, (error3, result3) => {
if (error3) {
console.error("Step 3 failed:", error3);
return;
}
console.log("All steps completed:", result3);
});
});
});Promise
Promise was introduced in ES6 as an async programming solution, solving the callback hell problem.
Promise Basics
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
const success = Math.random() > 0.5;
if (success) {
resolve("Operation successful!");
} else {
reject(new Error("Operation failed!"));
}
}, 1000);
});
// Using Promise
myPromise
.then(result => {
console.log("Success:", result);
})
.catch(error => {
console.error("Failure:", error.message);
})
.finally(() => {
console.log("Operation completed (regardless of outcome)");
});Promise States
// Three Promise states
// pending - Still executing
const pendingPromise = new Promise(() => {});
// fulfilled - Successfully resolved
const fulfilledPromise = Promise.resolve("Success");
// rejected - Failed
const rejectedPromise = Promise.reject(new Error("Failed"));Promise Chaining
// Promise chain
fetchUserData(1)
.then(user => {
console.log("Got user:", user);
return fetchUserPosts(user.id);
})
.then(posts => {
console.log("Got posts:", posts);
return posts.length;
})
.then(count => {
console.log("Post count:", count);
})
.catch(error => {
console.error("Operation failed:", error.message);
});Promise Static Methods
// Promise.all() - All must succeed
Promise.all([
Promise.resolve(1),
Promise.resolve(2),
Promise.resolve(3)
]).then(results => {
console.log("All Promises completed:", results); // [1, 2, 3]
});
// Promise.allSettled() - Wait for all to complete (ES2020)
Promise.allSettled([
Promise.resolve(1),
Promise.reject(new Error("Failed")),
Promise.resolve(3)
]).then(results => {
console.log("All settled:", results);
});
// Promise.race() - First to complete wins
const fast = new Promise(resolve => setTimeout(() => resolve("Fast"), 100));
const slow = new Promise(resolve => setTimeout(() => resolve("Slow"), 1000));
Promise.race([fast, slow])
.then(result => console.log("Winner:", result)); // "Fast"
// Promise.any() - First success wins (ES2021)
Promise.any([fast, slow])
.then(result => console.log("First success:", result));async/await
async/await was introduced in ES2017 as syntactic sugar, making async code look like sync code.
Basic Syntax
// async function
async function fetchData() {
try {
const response = await fetch("https://api.example.com/data");
const data = await response.json();
return data;
} catch (error) {
console.error("Failed to fetch data:", error);
throw error;
}
}
// Calling async function
fetchData()
.then(data => console.log("Data:", data))
.catch(error => console.error("Error:", error));Error Handling
// async/await error handling
async function processUserData() {
try {
console.log("Fetching user data");
const user = await fetchUserData(1);
console.log("User:", user);
console.log("Fetching user posts");
const posts = await fetchUserPosts(user.id);
console.log("Posts:", posts);
return { user, posts };
} catch (error) {
console.error("Error processing user data:", error.message);
throw new Error("User data processing failed: " + error.message);
}
}Parallel Execution
// Sequential execution (slower)
async function serialExecution() {
const user = await fetchUserData(1);
const posts = await fetchUserPosts(1);
const comments = await fetchUserComments(1);
return { user, posts, comments };
}
// Parallel execution (faster)
async function parallelExecution() {
const [user, posts, comments] = await Promise.all([
fetchUserData(1),
fetchUserPosts(1),
fetchUserComments(1)
]);
return { user, posts, comments };
}
// Mixed execution
async function mixedExecution() {
// First get user data
const user = await fetchUserData(1);
// Then fetch posts and comments in parallel
const [posts, comments] = await Promise.all([
fetchUserPosts(user.id),
fetchUserComments(user.id)
]);
return { user, posts, comments };
}Async Iterators and Generators
Async Generator
// Async generator function
async function* asyncNumberGenerator() {
let i = 0;
while (i < 5) {
await new Promise(resolve => setTimeout(resolve, 1000));
yield i++;
}
}
// Using async generator
async function useAsyncGenerator() {
for await (const number of asyncNumberGenerator()) {
console.log("Generated number:", number);
}
}Microtasks and Macrotasks
Understanding JavaScript's event loop is important for async programming.
// Event loop example
console.log("1. Synchronous code");
setTimeout(() => {
console.log("2. Macrotask (setTimeout)");
}, 0);
Promise.resolve().then(() => {
console.log("3. Microtask (Promise)");
});
console.log("4. Synchronous code ends");
// Output order:
// 1. Synchronous code
// 4. Synchronous code ends
// 3. Microtask (Promise)
// 2. Macrotask (setTimeout)Best Practices
1. Error Handling
// Unified error handling
class AsyncErrorHandler {
static async handle(asyncFunction, ...args) {
try {
const result = await asyncFunction(...args);
return { success: true, data: result };
} catch (error) {
console.error("Async operation failed:", error);
return { success: false, error: error.message };
}
}
static async retry(asyncFunction, maxRetries = 3, delay = 1000) {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
return await asyncFunction();
} catch (error) {
lastError = error;
console.log(`Attempt ${i + 1} failed: ${error.message}`);
if (i < maxRetries - 1) {
await this.delay(delay * (i + 1));
}
}
}
throw new Error(`Failed after ${maxRetries} retries: ${lastError.message}`);
}
static delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}2. Timeout Control
// Promise with timeout
class PromiseWithTimeout {
static async timeout(promise, ms) {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms);
});
return Promise.race([promise, timeoutPromise]);
}
}
// Usage
async function slowOperation() {
await new Promise(resolve => setTimeout(resolve, 3000));
return "Slow operation completed";
}
PromiseWithTimeout.timeout(slowOperation(), 2000)
.then(result => console.log("Result:", result))
.catch(error => console.error("Error:", error.message));3. Concurrency Control
// Limit concurrent requests
class ConcurrencyController {
constructor(limit = 3) {
this.limit = limit;
this.running = 0;
this.queue = [];
}
async add(asyncFunction) {
return new Promise((resolve, reject) => {
this.queue.push({ asyncFunction, resolve, reject });
this.process();
});
}
async process() {
if (this.running >= this.limit || this.queue.length === 0) {
return;
}
this.running++;
const { asyncFunction, resolve, reject } = this.queue.shift();
try {
const result = await asyncFunction();
resolve(result);
} catch (error) {
reject(error);
} finally {
this.running--;
this.process();
}
}
}Practical Example: API Client
class ApiClient {
constructor(baseURL, options = {}) {
this.baseURL = baseURL;
this.defaultOptions = {
timeout: 5000,
retries: 3,
...options
};
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const config = { ...this.defaultOptions, ...options };
let lastError;
for (let i = 0; i < config.retries; i++) {
try {
const response = await this.fetchWithTimeout(url, config);
return await this.handleResponse(response);
} catch (error) {
lastError = error;
console.log(`Attempt ${i + 1} failed: ${error.message}`);
if (i < config.retries - 1) {
await this.delay(1000 * (i + 1));
}
}
}
throw new Error(`Request failed after ${config.retries} retries: ${lastError.message}`);
}
async fetchWithTimeout(url, options) {
const { timeout, ...fetchOptions } = options;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...fetchOptions,
signal: controller.signal
});
return response;
} finally {
clearTimeout(timeoutId);
}
}
async handleResponse(response) {
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const contentType = response.headers.get("content-type");
if (contentType && contentType.includes("application/json")) {
return await response.json();
}
return await response.text();
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Convenience methods
async get(endpoint, options = {}) {
return this.request(endpoint, { method: "GET", ...options });
}
async post(endpoint, data, options = {}) {
return this.request(endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
...options
});
}
async put(endpoint, data, options = {}) {
return this.request(endpoint, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
...options
});
}
async delete(endpoint, options = {}) {
return this.request(endpoint, { method: "DELETE", ...options });
}
}
// Usage example
const api = new ApiClient("https://jsonplaceholder.typicode.com");
// api.get("/users/1")
// .then(user => console.log("User:", user))
// .catch(error => console.error("Failed:", error));Summary
Key points about JavaScript async programming:
- Basic Concept: Async programming allows programs to continue while waiting for operations
- Callbacks: Earliest async method, but can lead to callback hell
- Promise: ES6 solution supporting chaining and error handling
- async/await: ES2017 syntactic sugar making async code look synchronous
- Promise Static Methods: all(), race(), allSettled(), any()
- Async Iteration: Async generators and iterators
- Event Loop: Understanding microtasks and macrotasks execution order
- Best Practices: Error handling, timeout control, concurrency control
- Practical Applications: API clients, data loaders
Mastering async programming is a key skill for building modern JavaScript applications. In the next chapter, we will learn about JavaScript form handling.