Master asynchronous programming with promises, async/await, and generators.
Master asynchronous programming with promises, async/await, and generators.
Learn to work with promises, handle asynchronous operations, and manage promise chains for better error handling and code organization
Content by: Nishant Darji
JavaScript Developer
Promises represent the eventual completion (or failure) of an asynchronous operation and its resulting value. They provide a cleaner alternative to callback-based asynchronous code and help avoid callback hell.
// Creating promises
// Basic promise
const basicPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Success!');
}, 1000);
});
// Promise with error
const errorPromise = new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('Something went wrong!'));
}, 1000);
});
// Promise with conditional logic
function fetchUserData(userId) {
return new Promise((resolve, reject) => {
if (!userId) {
reject(new Error('User ID is required'));
return;
}
// Simulate API call
setTimeout(() => {
const userData = {
id: userId,
name: 'John Doe',
email: 'john@example.com'
};
resolve(userData);
}, 1000);
});
}
// Promise with timeout
function promiseWithTimeout(promise, timeout) {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), timeout)
)
]);
}
// Using promises
basicPromise
.then(result => console.log(result))
.catch(error => console.error(error));
errorPromise
.then(result => console.log(result))
.catch(error => console.error(error.message));
fetchUserData(123)
.then(user => console.log('User:', user))
.catch(error => console.error('Error:', error.message));
// Promise chaining
fetchUserData(123)
.then(user => {
console.log('User fetched:', user);
return fetchUserPosts(user.id);
})
.then(posts => {
console.log('Posts fetched:', posts);
return fetchUserComments(user.id);
})
.then(comments => {
console.log('Comments fetched:', comments);
})
.catch(error => {
console.error('Error in chain:', error.message);
});
// Promise.all - Wait for all promises
const promises = [
fetchUserData(1),
fetchUserData(2),
fetchUserData(3)
];
Promise.all(promises)
.then(users => {
console.log('All users:', users);
})
.catch(error => {
console.error('One of the promises failed:', error);
});
// Promise.allSettled - Wait for all promises to settle
Promise.allSettled(promises)
.then(results => {
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Promise ${index} succeeded:`, result.value);
} else {
console.log(`Promise ${index} failed:`, result.reason);
}
});
});
// Promise.race - First promise to settle
const fastPromise = new Promise(resolve => setTimeout(() => resolve('Fast'), 100));
const slowPromise = new Promise(resolve => setTimeout(() => resolve('Slow'), 1000));
Promise.race([fastPromise, slowPromise])
.then(result => console.log('Winner:', result)); // "Fast"
// Promise.any - First promise to fulfill
const errorPromise = new Promise((_, reject) => setTimeout(() => reject('Error'), 500));
const successPromise = new Promise(resolve => setTimeout(() => resolve('Success'), 1000));
Promise.any([errorPromise, successPromise])
.then(result => console.log('First success:', result)) // "Success"
.catch(error => console.log('All promises failed:', error));
// Promise.resolve and Promise.reject
const resolvedPromise = Promise.resolve('Immediate success');
const rejectedPromise = Promise.reject(new Error('Immediate error'));
// Converting callback-based functions to promises
function promisify(fn) {
return function(...args) {
return new Promise((resolve, reject) => {
fn(...args, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
};
}
// Example usage
const fs = require('fs');
const readFileAsync = promisify(fs.readFile);
readFileAsync('file.txt', 'utf8')
.then(content => console.log('File content:', content))
.catch(error => console.error('File read error:', error));
// Exercise: Build a Promise-based API Client
class ApiClient {
constructor(baseUrl) {
this.baseUrl = baseUrl;
this.cache = new Map();
}
request(endpoint, options = {}) {
const url = `${this.baseUrl}${endpoint}`;
const cacheKey = `${options.method || 'GET'}-${url}`;
// Check cache for GET requests
if (options.method === 'GET' && this.cache.has(cacheKey)) {
return Promise.resolve(this.cache.get(cacheKey));
}
return fetch(url, {
headers: {
'Content-Type': 'application/json',
...options.headers
},
...options
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
})
.then(data => {
// Cache GET responses
if (options.method === 'GET') {
this.cache.set(cacheKey, data);
}
return data;
});
}
get(endpoint) {
return this.request(endpoint, { method: 'GET' });
}
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' });
}
clearCache() {
this.cache.clear();
}
}
// Exercise: Build a Promise Queue
class PromiseQueue {
constructor() {
this.queue = [];
this.running = false;
}
add(task) {
return new Promise((resolve, reject) => {
this.queue.push({
task,
resolve,
reject
});
if (!this.running) {
this.process();
}
});
}
async process() {
this.running = true;
while (this.queue.length > 0) {
const { task, resolve, reject } = this.queue.shift();
try {
const result = await task();
resolve(result);
} catch (error) {
reject(error);
}
}
this.running = false;
}
getLength() {
return this.queue.length;
}
clear() {
this.queue = [];
}
}
// Exercise: Build a Retry Mechanism
function retry(fn, maxAttempts = 3, delay = 1000) {
return new Promise((resolve, reject) => {
let attempts = 0;
function attempt() {
attempts++;
fn()
.then(resolve)
.catch(error => {
if (attempts >= maxAttempts) {
reject(error);
} else {
console.log(`Attempt ${attempts} failed, retrying in ${delay}ms...`);
setTimeout(attempt, delay);
}
});
}
attempt();
});
}
// Test the exercises
const api = new ApiClient('https://api.example.com');
// Test API client
api.get('/users')
.then(users => console.log('Users:', users))
.catch(error => console.error('API error:', error));
// Test promise queue
const queue = new PromiseQueue();
queue.add(() => new Promise(resolve => setTimeout(() => resolve('Task 1'), 1000)));
queue.add(() => new Promise(resolve => setTimeout(() => resolve('Task 2'), 500)));
queue.add(() => new Promise(resolve => setTimeout(() => resolve('Task 3'), 200)));
// Test retry mechanism
const unreliableFunction = () => {
return new Promise((resolve, reject) => {
if (Math.random() > 0.5) {
resolve('Success!');
} else {
reject(new Error('Random failure'));
}
});
};
retry(unreliableFunction, 3, 1000)
.then(result => console.log('Retry succeeded:', result))
.catch(error => console.error('Retry failed:', error));
Test your understanding of this topic:
Master async/await syntax, the modern way to work with promises that makes asynchronous code look and behave more like synchronous code
Content by: Bansi Patel
Node.js Developer
Async/await is built on top of promises and provides a more intuitive way to write asynchronous code. It allows you to write promise-based code as if it were synchronous, without blocking the main thread.
// Basic async function
async function fetchUserData(userId) {
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const userData = await response.json();
return userData;
} catch (error) {
console.error('Error fetching user:', error.message);
throw error;
}
}
// Async function with multiple awaits
async function processUser(userId) {
try {
// Fetch user data
const user = await fetchUserData(userId);
console.log('User fetched:', user);
// Fetch user posts
const posts = await fetchUserPosts(userId);
console.log('Posts fetched:', posts);
// Fetch user comments
const comments = await fetchUserComments(userId);
console.log('Comments fetched:', comments);
return {
user,
posts,
comments
};
} catch (error) {
console.error('Error processing user:', error.message);
throw error;
}
}
// Async function with parallel execution
async function processUserParallel(userId) {
try {
// Execute all promises in parallel
const [user, posts, comments] = await Promise.all([
fetchUserData(userId),
fetchUserPosts(userId),
fetchUserComments(userId)
]);
return { user, posts, comments };
} catch (error) {
console.error('Error in parallel processing:', error.message);
throw error;
}
}
// Async function with conditional logic
async function fetchDataWithFallback(primaryUrl, fallbackUrl) {
try {
const response = await fetch(primaryUrl);
if (response.ok) {
return await response.json();
}
} catch (error) {
console.log('Primary URL failed, trying fallback...');
}
try {
const response = await fetch(fallbackUrl);
if (response.ok) {
return await response.json();
}
} catch (error) {
throw new Error('Both URLs failed');
}
}
// Async function with timeout
async function fetchWithTimeout(url, timeout = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
signal: controller.signal
});
clearTimeout(timeoutId);
return await response.json();
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error('Request timeout');
}
throw error;
}
}
// Async function with retry logic
async function fetchWithRetry(url, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url);
if (response.ok) {
return await response.json();
}
} catch (error) {
if (attempt === maxRetries) {
throw error;
}
console.log(`Attempt ${attempt} failed, retrying...`);
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
}
}
}
// Async function in loops
async function processItems(items) {
const results = [];
// Sequential processing
for (const item of items) {
const result = await processItem(item);
results.push(result);
}
return results;
}
async function processItemsParallel(items) {
// Parallel processing
const promises = items.map(item => processItem(item));
return await Promise.all(promises);
}
// Async function with error boundaries
async function safeAsyncOperation(operation) {
try {
return await operation();
} catch (error) {
console.error('Operation failed:', error.message);
return null;
}
}
// Exercise: Build an Async Data Processor
class AsyncDataProcessor {
constructor() {
this.cache = new Map();
this.processing = new Set();
}
async processData(data, processor) {
const cacheKey = JSON.stringify(data);
if (this.cache.has(cacheKey)) {
console.log('Returning cached result');
return this.cache.get(cacheKey);
}
if (this.processing.has(cacheKey)) {
console.log('Waiting for ongoing processing...');
while (this.processing.has(cacheKey)) {
await new Promise(resolve => setTimeout(resolve, 100));
}
return this.cache.get(cacheKey);
}
this.processing.add(cacheKey);
try {
const result = await processor(data);
this.cache.set(cacheKey, result);
return result;
} finally {
this.processing.delete(cacheKey);
}
}
async batchProcess(items, processor, batchSize = 5) {
const results = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const batchPromises = batch.map(item => processor(item));
const batchResults = await Promise.all(batchPromises);
results.push(...batchResults);
console.log(`Processed batch ${Math.floor(i / batchSize) + 1}`);
}
return results;
}
async processWithProgress(items, processor, onProgress) {
const results = [];
let completed = 0;
const promises = items.map(async (item, index) => {
const result = await processor(item);
completed++;
onProgress(completed, items.length, result);
return result;
});
return await Promise.all(promises);
}
}
// Exercise: Build an Async Cache Manager
class AsyncCacheManager {
constructor() {
this.cache = new Map();
this.expiryTimes = new Map();
}
async get(key) {
if (this.isExpired(key)) {
this.delete(key);
return null;
}
return this.cache.get(key);
}
set(key, value, ttl = 60000) { // Default 1 minute TTL
this.cache.set(key, value);
this.expiryTimes.set(key, Date.now() + ttl);
}
delete(key) {
this.cache.delete(key);
this.expiryTimes.delete(key);
}
isExpired(key) {
const expiryTime = this.expiryTimes.get(key);
return expiryTime && Date.now() > expiryTime;
}
async getOrSet(key, fetcher, ttl = 60000) {
let value = await this.get(key);
if (value === null) {
value = await fetcher();
this.set(key, value, ttl);
}
return value;
}
clear() {
this.cache.clear();
this.expiryTimes.clear();
}
getStats() {
return {
size: this.cache.size,
keys: Array.from(this.cache.keys())
};
}
}
// Exercise: Build an Async Task Scheduler
class AsyncTaskScheduler {
constructor(maxConcurrent = 3) {
this.maxConcurrent = maxConcurrent;
this.running = 0;
this.queue = [];
}
async schedule(task, priority = 0) {
return new Promise((resolve, reject) => {
this.queue.push({
task,
priority,
resolve,
reject
});
this.queue.sort((a, b) => b.priority - a.priority);
this.process();
});
}
async process() {
if (this.running >= this.maxConcurrent || this.queue.length === 0) {
return;
}
const { task, resolve, reject } = this.queue.shift();
this.running++;
try {
const result = await task();
resolve(result);
} catch (error) {
reject(error);
} finally {
this.running--;
this.process(); // Process next task
}
}
getStats() {
return {
running: this.running,
queued: this.queue.length,
maxConcurrent: this.maxConcurrent
};
}
}
// Test the exercises
const processor = new AsyncDataProcessor();
const cache = new AsyncCacheManager();
const scheduler = new AsyncTaskScheduler(2);
// Test data processor
const data = [1, 2, 3, 4, 5];
const processorFn = async (item) => {
await new Promise(resolve => setTimeout(resolve, 1000));
return item * 2;
};
processor.processData(data, processorFn)
.then(result => console.log('Processed result:', result));
// Test cache manager
cache.getOrSet('user:123', async () => {
await new Promise(resolve => setTimeout(resolve, 1000));
return { id: 123, name: 'John' };
})
.then(user => console.log('Cached user:', user));
// Test task scheduler
const tasks = [
() => new Promise(resolve => setTimeout(() => resolve('Task 1'), 2000)),
() => new Promise(resolve => setTimeout(() => resolve('Task 2'), 1000)),
() => new Promise(resolve => setTimeout(() => resolve('Task 3'), 1500)),
() => new Promise(resolve => setTimeout(() => resolve('Task 4'), 500))
];
tasks.forEach((task, index) => {
scheduler.schedule(task, index)
.then(result => console.log(`Scheduled task ${index + 1}: ${result}`));
});
Test your understanding of this topic:
Learn about generators, iterators, and how they can be used for advanced asynchronous programming patterns and custom iteration
Content by: Raj Koradiya
Node.js Developer
Generators are functions that can be paused and resumed, allowing for more complex control flow and the creation of custom iterators. They're particularly useful for handling asynchronous operations and creating infinite sequences.
// Basic generator function
function* numberGenerator() {
yield 1;
yield 2;
yield 3;
}
const gen = numberGenerator();
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
console.log(gen.next().value); // 3
console.log(gen.next().done); // true
// Generator with parameters
function* fibonacci() {
let [prev, curr] = [0, 1];
while (true) {
yield curr;
[prev, curr] = [curr, prev + curr];
}
}
const fib = fibonacci();
for (let i = 0; i < 10; i++) {
console.log(fib.next().value);
}
// Generator with return
function* generatorWithReturn() {
yield 1;
yield 2;
return 'finished';
yield 3; // This won't execute
}
const gen2 = generatorWithReturn();
console.log(gen2.next()); // { value: 1, done: false }
console.log(gen2.next()); // { value: 2, done: false }
console.log(gen2.next()); // { value: 'finished', done: true }
// Generator with throw
function* generatorWithError() {
try {
yield 1;
yield 2;
} catch (error) {
console.log('Caught error:', error.message);
yield 3;
}
}
const gen3 = generatorWithError();
console.log(gen3.next().value); // 1
console.log(gen3.throw(new Error('Something went wrong')).value); // 3
// Custom iterator with generator
const customIterable = {
*[Symbol.iterator]() {
yield 'first';
yield 'second';
yield 'third';
}
};
for (const item of customIterable) {
console.log(item);
}
// Generator for data processing
function* dataProcessor(data) {
for (const item of data) {
if (item > 0) {
yield item * 2;
}
}
}
const numbers = [1, -2, 3, -4, 5];
const processed = Array.from(dataProcessor(numbers));
console.log(processed); // [2, 6, 10]
// Async generator
async function* asyncNumberGenerator() {
for (let i = 1; i <= 5; i++) {
await new Promise(resolve => setTimeout(resolve, 1000));
yield i;
}
}
async function consumeAsyncGenerator() {
for await (const num of asyncNumberGenerator()) {
console.log(num);
}
}
// Async generator for API pagination
async function* fetchPaginatedData(url) {
let page = 1;
let hasMore = true;
while (hasMore) {
try {
const response = await fetch(`${url}?page=${page}`);
const data = await response.json();
if (data.items && data.items.length > 0) {
yield* data.items;
page++;
} else {
hasMore = false;
}
} catch (error) {
console.error('Error fetching data:', error);
hasMore = false;
}
}
}
// Async generator for file reading
async function* readFileLines(filePath) {
const fileHandle = await fs.open(filePath, 'r');
const reader = fileHandle.createReadStream();
for await (const chunk of reader) {
const lines = chunk.toString().split('\n');
for (const line of lines) {
if (line.trim()) {
yield line.trim();
}
}
}
await fileHandle.close();
}
// Async generator with error handling
async function* robustDataFetcher(urls) {
for (const url of urls) {
try {
const response = await fetch(url);
if (response.ok) {
const data = await response.json();
yield { url, data, success: true };
} else {
yield { url, error: `HTTP ${response.status}`, success: false };
}
} catch (error) {
yield { url, error: error.message, success: false };
}
}
}
// Generator for state management
function* stateMachine() {
let state = 'idle';
while (true) {
const action = yield state;
switch (action) {
case 'start':
state = 'running';
break;
case 'pause':
state = 'paused';
break;
case 'stop':
state = 'stopped';
break;
case 'reset':
state = 'idle';
break;
}
}
}
// Test async generators
async function testAsyncGenerators() {
console.log('Starting async generator test...');
for await (const num of asyncNumberGenerator()) {
console.log(`Received: ${num}`);
}
console.log('Async generator test completed');
}
// Test state machine
const machine = stateMachine();
console.log(machine.next().value); // 'idle'
console.log(machine.next('start').value); // 'running'
console.log(machine.next('pause').value); // 'paused'
console.log(machine.next('stop').value); // 'stopped'
// Exercise: Build a Generator-based Pipeline
class DataPipeline {
constructor() {
this.stages = [];
}
addStage(stage) {
this.stages.push(stage);
return this;
}
*process(data) {
let result = data;
for (const stage of this.stages) {
if (typeof stage === 'function') {
result = stage(result);
} else if (stage && typeof stage.process === 'function') {
result = stage.process(result);
}
yield result;
}
}
async *processAsync(data) {
let result = data;
for (const stage of this.stages) {
if (typeof stage === 'function') {
result = await stage(result);
} else if (stage && typeof stage.processAsync === 'function') {
result = await stage.processAsync(result);
}
yield result;
}
}
}
// Exercise: Build a Generator-based Event Emitter
class GeneratorEventEmitter {
constructor() {
this.listeners = new Map();
this.generators = new Map();
}
on(event, generator) {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event).push(generator);
}
emit(event, data) {
const eventGenerators = this.listeners.get(event) || [];
eventGenerators.forEach(generator => {
if (typeof generator === 'function') {
generator(data);
} else if (generator && typeof generator.next === 'function') {
generator.next(data);
}
});
}
*createListener(event) {
while (true) {
const data = yield;
console.log(`Event ${event} received:`, data);
}
}
}
// Exercise: Build a Generator-based State Manager
class GeneratorStateManager {
constructor(initialState = {}) {
this.state = { ...initialState };
this.history = [];
this.generators = new Map();
}
*createStateGenerator() {
while (true) {
const action = yield this.state;
if (action.type === 'UPDATE') {
this.state = { ...this.state, ...action.payload };
this.history.push({ ...this.state });
} else if (action.type === 'RESET') {
this.state = { ...this.initialState };
this.history = [];
} else if (action.type === 'UNDO') {
if (this.history.length > 0) {
this.history.pop();
this.state = this.history.length > 0
? this.history[this.history.length - 1]
: { ...this.initialState };
}
}
}
}
update(payload) {
const generator = this.generators.get('main');
if (generator) {
generator.next({ type: 'UPDATE', payload });
}
}
reset() {
const generator = this.generators.get('main');
if (generator) {
generator.next({ type: 'RESET' });
}
}
undo() {
const generator = this.generators.get('main');
if (generator) {
generator.next({ type: 'UNDO' });
}
}
getState() {
return { ...this.state };
}
getHistory() {
return [...this.history];
}
}
// Test the exercises
const pipeline = new DataPipeline()
.addStage(data => data.filter(x => x > 0))
.addStage(data => data.map(x => x * 2))
.addStage(data => data.reduce((sum, x) => sum + x, 0));
const data = [-1, 2, -3, 4, -5, 6];
const results = Array.from(pipeline.process(data));
console.log('Pipeline results:', results);
const emitter = new GeneratorEventEmitter();
const listener = emitter.createListener('user-action');
emitter.on('user-action', listener);
emitter.emit('user-action', { type: 'click', target: 'button' });
const stateManager = new GeneratorStateManager({ count: 0, user: null });
const stateGen = stateManager.createStateGenerator();
stateManager.generators.set('main', stateGen);
stateManager.update({ count: 1 });
stateManager.update({ user: { name: 'John' } });
console.log('Current state:', stateManager.getState());
stateManager.undo();
console.log('After undo:', stateManager.getState());
Test your understanding of this topic:
Continue your learning journey and master the next set of concepts.
Continue to Module 7