Skip to main content

Module 17: Promises

Promises represent the eventual completion (or failure) of an asynchronous operation. They provide a cleaner, more elegant way to handle async code than callbacks.


1. Understanding Promises

1.1 What is a Promise?

A Promise is an object representing the eventual completion or failure of an async operation.

Promise States:

  • Pending – Initial state, neither fulfilled nor rejected
  • Fulfilled – Operation completed successfully
  • Rejected – Operation failed
const promise = new Promise((resolve, reject) => {
// Async operation
setTimeout(() => {
const success = true;

if (success) {
resolve('Operation successful!'); // Fulfill
} else {
reject('Operation failed!'); // Reject
}
}, 1000);
});

console.log(promise); // Promise { <pending> }

1.2 Why Promises?

// ❌ Callback Hell
getUserData(userId, function(error, user) {
if (error) return console.error(error);

getOrders(user.id, function(error, orders) {
if (error) return console.error(error);

getOrderDetails(orders[0].id, function(error, details) {
if (error) return console.error(error);

console.log(details);
});
});
});

// ✅ Promise Chain
getUserData(userId)
.then(user => getOrders(user.id))
.then(orders => getOrderDetails(orders[0].id))
.then(details => console.log(details))
.catch(error => console.error(error));

2. Creating Promises

2.1 Basic Promise

const myPromise = new Promise((resolve, reject) => {
// resolve(value) - Call when successful
// reject(reason) - Call when failed

const success = true;

if (success) {
resolve('Success!');
} else {
reject('Failed!');
}
});

2.2 Async Operation Example

function fetchUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const users = {
1: { id: 1, name: 'John' },
2: { id: 2, name: 'Jane' }
};

const user = users[id];

if (user) {
resolve(user);
} else {
reject(new Error('User not found'));
}
}, 1000);
});
}

// Usage
fetchUser(1)
.then(user => console.log('User:', user))
.catch(error => console.error('Error:', error.message));

2.3 Promise.resolve() / Promise.reject()

// Create fulfilled promise
const fulfilled = Promise.resolve('Success');
fulfilled.then(value => console.log(value)); // "Success"

// Create rejected promise
const rejected = Promise.reject('Error');
rejected.catch(error => console.error(error)); // "Error"

// Wrapping values
Promise.resolve(42).then(value => console.log(value)); // 42
Promise.resolve([1, 2, 3]).then(arr => console.log(arr)); // [1, 2, 3]

3. Consuming Promises

3.1 then()

const promise = fetchUser(1);

// Single then
promise.then(user => {
console.log('User:', user);
});

// Chaining then
promise
.then(user => {
console.log('User:', user);
return user.name;
})
.then(name => {
console.log('Name:', name);
return name.toUpperCase();
})
.then(upperName => {
console.log('Upper name:', upperName);
});

3.2 catch()

fetchUser(999)
.then(user => console.log(user))
.catch(error => {
console.error('Error occurred:', error.message);
});

// Catch in chain
fetchUser(1)
.then(user => {
if (!user.email) {
throw new Error('User has no email');
}
return user.email;
})
.then(email => console.log('Email:', email))
.catch(error => console.error('Error:', error.message));

3.3 finally()

let isLoading = true;

fetchUser(1)
.then(user => {
console.log('User:', user);
})
.catch(error => {
console.error('Error:', error);
})
.finally(() => {
isLoading = false;
console.log('Request completed');
});

4. Promise Chaining

4.1 Sequential Operations

fetchUser(1)
.then(user => {
console.log('User:', user);
return getOrders(user.id); // Return new promise
})
.then(orders => {
console.log('Orders:', orders);
return getOrderDetails(orders[0].id);
})
.then(details => {
console.log('Details:', details);
})
.catch(error => {
console.error('Error in chain:', error);
});

4.2 Returning Values

Promise.resolve(5)
.then(num => {
console.log(num); // 5
return num * 2;
})
.then(num => {
console.log(num); // 10
return num + 3;
})
.then(num => {
console.log(num); // 13
});

4.3 Returning Promises

function step1() {
return new Promise(resolve => {
setTimeout(() => resolve('Step 1 done'), 1000);
});
}

function step2(prevResult) {
return new Promise(resolve => {
setTimeout(() => resolve(prevResult + ' → Step 2 done'), 1000);
});
}

function step3(prevResult) {
return new Promise(resolve => {
setTimeout(() => resolve(prevResult + ' → Step 3 done'), 1000);
});
}

step1()
.then(result => {
console.log(result);
return step2(result);
})
.then(result => {
console.log(result);
return step3(result);
})
.then(result => {
console.log(result);
});

5. Error Handling

5.1 Propagation

fetchUser(1)
.then(user => {
console.log('User:', user);
throw new Error('Something went wrong');
})
.then(result => {
// This won't execute
console.log('This will be skipped');
})
.catch(error => {
// Error is caught here
console.error('Caught:', error.message);
});

5.2 Recovery

fetchUser(999)
.then(user => console.log(user))
.catch(error => {
console.error('Error:', error.message);
// Return fallback value
return { id: 0, name: 'Guest' };
})
.then(user => {
// Continues with fallback user
console.log('User:', user);
});

5.3 Multiple Catch Blocks

fetchUser(1)
.then(user => {
if (!user.active) {
throw new Error('User inactive');
}
return getOrders(user.id);
})
.catch(error => {
console.error('User error:', error);
throw error; // Re-throw
})
.then(orders => {
return processOrders(orders);
})
.catch(error => {
console.error('Order error:', error);
});

6. Promise Static Methods

6.1 Promise.all()

// Wait for all promises to resolve
const promise1 = fetchUser(1);
const promise2 = fetchUser(2);
const promise3 = fetchUser(3);

Promise.all([promise1, promise2, promise3])
.then(users => {
console.log('All users:', users);
// [user1, user2, user3]
})
.catch(error => {
// If any promise rejects
console.error('Error:', error);
});

// Practical example
Promise.all([
fetch('https://api.example.com/users'),
fetch('https://api.example.com/posts'),
fetch('https://api.example.com/comments')
])
.then(responses => Promise.all(responses.map(r => r.json())))
.then(([users, posts, comments]) => {
console.log('Users:', users);
console.log('Posts:', posts);
console.log('Comments:', comments);
})
.catch(error => console.error('Error:', error));

6.2 Promise.allSettled()

// Wait for all promises to settle (fulfill or reject)
const promises = [
fetchUser(1),
fetchUser(999), // Will fail
fetchUser(2)
];

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);
}
});
});

// Output:
// Promise 0 succeeded: { id: 1, name: 'John' }
// Promise 1 failed: Error: User not found
// Promise 2 succeeded: { id: 2, name: 'Jane' }

6.3 Promise.race()

// Returns first promise to settle
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(result)) // "Fast"
.catch(error => console.error(error));

// Timeout pattern
function fetchWithTimeout(url, timeout = 5000) {
return Promise.race([
fetch(url),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), timeout)
)
]);
}

fetchWithTimeout('https://api.example.com/data', 3000)
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error.message));

6.4 Promise.any()

// Returns first fulfilled promise (ignores rejections)
const p1 = Promise.reject('Error 1');
const p2 = new Promise(resolve => setTimeout(() => resolve('Success'), 100));
const p3 = Promise.reject('Error 3');

Promise.any([p1, p2, p3])
.then(result => console.log(result)) // "Success"
.catch(error => console.error(error));

// If all reject
Promise.any([
Promise.reject('Error 1'),
Promise.reject('Error 2'),
Promise.reject('Error 3')
])
.then(result => console.log(result))
.catch(error => {
console.error('All failed:', error); // AggregateError
});

7. Promise Patterns

7.1 Sequential Execution

const tasks = [
() => fetchUser(1),
() => fetchUser(2),
() => fetchUser(3)
];

// Execute sequentially
function sequential(tasks) {
return tasks.reduce((promise, task) => {
return promise.then(results => {
return task().then(result => [...results, result]);
});
}, Promise.resolve([]));
}

sequential(tasks)
.then(results => console.log('All results:', results))
.catch(error => console.error('Error:', error));

7.2 Retry Logic

function retry(fn, maxAttempts, delay = 1000) {
return new Promise((resolve, reject) => {
let attempts = 0;

function attempt() {
attempts++;

fn()
.then(resolve)
.catch(error => {
if (attempts < maxAttempts) {
console.log(`Attempt ${attempts} failed, retrying...`);
setTimeout(attempt, delay);
} else {
reject(error);
}
});
}

attempt();
});
}

// Usage
retry(() => fetchUser(999), 3, 1000)
.then(user => console.log('Success:', user))
.catch(error => console.error('All attempts failed:', error.message));

7.3 Debounced Promise

function debouncePromise(fn, delay) {
let timeoutId;
let latestResolve;
let latestReject;

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

return new Promise((resolve, reject) => {
latestResolve = resolve;
latestReject = reject;

timeoutId = setTimeout(() => {
fn(...args)
.then(latestResolve)
.catch(latestReject);
}, delay);
});
};
}

// Usage
const debouncedFetch = debouncePromise(fetchUser, 300);

// Only last call executes
debouncedFetch(1);
debouncedFetch(2);
debouncedFetch(3).then(user => console.log('User:', user));

7.4 Promise Queue

class PromiseQueue {
constructor(concurrency = 1) {
this.concurrency = concurrency;
this.running = 0;
this.queue = [];
}

add(promiseGenerator) {
return new Promise((resolve, reject) => {
this.queue.push({ promiseGenerator, resolve, reject });
this.process();
});
}

process() {
if (this.running >= this.concurrency || this.queue.length === 0) {
return;
}

this.running++;
const { promiseGenerator, resolve, reject } = this.queue.shift();

promiseGenerator()
.then(resolve)
.catch(reject)
.finally(() => {
this.running--;
this.process();
});
}
}

// Usage
const queue = new PromiseQueue(2); // Max 2 concurrent

queue.add(() => fetchUser(1)).then(console.log);
queue.add(() => fetchUser(2)).then(console.log);
queue.add(() => fetchUser(3)).then(console.log);

8. Practical Examples

8.1 API Client

class APIClient {
constructor(baseURL) {
this.baseURL = baseURL;
}

async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;

try {
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});

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

return await response.json();
} catch (error) {
console.error('API request failed:', error);
throw error;
}
}

get(endpoint) {
return this.request(endpoint, { method: 'GET' });
}

post(endpoint, data) {
return this.request(endpoint, {
method: 'POST',
body: JSON.stringify(data)
});
}
}

// Usage
const api = new APIClient('https://api.example.com');

api.get('/users')
.then(users => console.log('Users:', users))
.catch(error => console.error('Error:', error));

8.2 Image Loader

function loadImage(url) {
return new Promise((resolve, reject) => {
const img = new Image();

img.onload = () => resolve(img);
img.onerror = () => reject(new Error(`Failed to load image: ${url}`));

img.src = url;
});
}

// Load single image
loadImage('/image.jpg')
.then(img => {
document.body.appendChild(img);
})
.catch(error => console.error(error));

// Load multiple images
const imageUrls = ['/img1.jpg', '/img2.jpg', '/img3.jpg'];

Promise.all(imageUrls.map(url => loadImage(url)))
.then(images => {
images.forEach(img => document.body.appendChild(img));
})
.catch(error => console.error('Failed to load images:', error));

8.3 Cache with Promises

class PromiseCache {
constructor() {
this.cache = new Map();
}

get(key, promiseGenerator) {
if (this.cache.has(key)) {
return Promise.resolve(this.cache.get(key));
}

return promiseGenerator().then(result => {
this.cache.set(key, result);
return result;
});
}

clear() {
this.cache.clear();
}
}

// Usage
const cache = new PromiseCache();

cache.get('user:1', () => fetchUser(1))
.then(user => console.log('User:', user));

// Second call returns cached result
cache.get('user:1', () => fetchUser(1))
.then(user => console.log('Cached user:', user));

9. Common Mistakes

9.1 Not Returning Promises

// ❌ Bad
fetchUser(1)
.then(user => {
getOrders(user.id); // Forgot to return!
})
.then(orders => {
// orders is undefined
console.log(orders);
});

// ✅ Good
fetchUser(1)
.then(user => {
return getOrders(user.id); // Return promise
})
.then(orders => {
console.log(orders);
});

9.2 Nested Promises

// ❌ Bad (callback hell with promises)
fetchUser(1)
.then(user => {
getOrders(user.id)
.then(orders => {
getOrderDetails(orders[0].id)
.then(details => {
console.log(details);
});
});
});

// ✅ Good (flat chain)
fetchUser(1)
.then(user => getOrders(user.id))
.then(orders => getOrderDetails(orders[0].id))
.then(details => console.log(details));

9.3 Not Handling Errors

// ❌ Bad
fetchUser(1)
.then(user => console.log(user));

// ✅ Good
fetchUser(1)
.then(user => console.log(user))
.catch(error => console.error('Error:', error));

Summary

In this module, you learned:

  • ✅ What Promises are and their three states
  • ✅ Creating and consuming Promises
  • ✅ Promise chaining for sequential operations
  • ✅ Error handling with catch() and finally()
  • ✅ Promise static methods: all, allSettled, race, any
  • ✅ Common patterns: retry, sequential, queue
  • ✅ Practical examples and real-world usage
  • ✅ Common mistakes and how to avoid them
Next Steps

In Module 18, you'll learn about Async/Await, which makes working with Promises even more elegant.


Practice Exercises

  1. Convert callback-based functions to Promises
  2. Build a retry mechanism with exponential backoff
  3. Implement Promise.all() from scratch
  4. Create a rate-limited API client
  5. Build an image preloader with progress tracking
  6. Implement a promise-based timeout wrapper
  7. Create a caching layer for API requests
  8. Build a sequential task runner

Additional Resources