🤝 Promises

ES6 Asynchronous Programming Solution

What are Promises?

A Promise is an object representing the eventual completion or failure of an asynchronous operation. It's a cleaner alternative to callbacks.

🎭 Promise States

// Three states:
// 1. Pending - initial state, not fulfilled or rejected
// 2. Fulfilled - operation completed successfully
// 3. Rejected - operation failed

let promise = new Promise((resolve, reject) => {
    // Pending...
    
    // Fulfill
    resolve('Success!');
    
    // Or reject
    // reject('Error!');
});

// Once settled (fulfilled or rejected), state cannot change

🏗️ Creating Promises

Promise Constructor

// Create a promise
let promise = new Promise((resolve, reject) => {
    // Async operation
    let success = true;
    
    if (success) {
        resolve('Operation succeeded');
    } else {
        reject('Operation failed');
    }
});

// With setTimeout
let delayed = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('Done after 2 seconds');
    }, 2000);
});

// Immediate resolve
let immediate = Promise.resolve('Already resolved');

// Immediate reject
let failed = Promise.reject('Already rejected');

// Real example: fetch data
function fetchUser(id) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (id > 0) {
                resolve({ id, name: 'John' });
            } else {
                reject('Invalid ID');
            }
        }, 1000);
    });
}

Promise Methods

// Promise.resolve - create fulfilled promise
let promise = Promise.resolve(42);
promise.then(value => console.log(value));  // 42

// Promise.reject - create rejected promise
let promise = Promise.reject('Error');
promise.catch(error => console.error(error));  // 'Error'

// Wrap value in promise
let promise = Promise.resolve({ name: 'John' });

// If value is promise, returns it
let p1 = Promise.resolve(42);
let p2 = Promise.resolve(p1);
console.log(p1 === p2);  // true

⛓️ Promise Chaining

then() Method

// Handle fulfilled promise
let promise = Promise.resolve(5);

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

// Chain operations
fetchUser(1)
    .then(user => {
        console.log('User:', user.name);
        return fetchPosts(user.id);
    })
    .then(posts => {
        console.log('Posts:', posts.length);
        return fetchComments(posts[0].id);
    })
    .then(comments => {
        console.log('Comments:', comments.length);
    });

// Return promise from then
fetch('/api/user')
    .then(response => response.json())  // Returns promise
    .then(user => {
        console.log(user);
        return fetch(`/api/posts?user=${user.id}`);
    })
    .then(response => response.json())
    .then(posts => console.log(posts));

catch() Method

// Handle rejection
let promise = Promise.reject('Error occurred');

promise.catch(error => {
    console.error(error);  // 'Error occurred'
});

// Chain with catch
fetchUser(1)
    .then(user => fetchPosts(user.id))
    .then(posts => console.log(posts))
    .catch(error => {
        console.error('Failed:', error);
    });

// Catch handles any error in chain
Promise.resolve(5)
    .then(value => {
        throw new Error('Oops!');
    })
    .then(value => {
        console.log('This will not run');
    })
    .catch(error => {
        console.error('Caught:', error.message);  // 'Oops!'
    });

// Multiple catch blocks
fetchData()
    .then(processData)
    .catch(error => {
        console.error('Processing failed:', error);
        return defaultData;  // Recover
    })
    .then(data => {
        console.log('Using data:', data);
    })
    .catch(error => {
        console.error('Fatal error:', error);
    });

finally() Method

// Runs regardless of outcome (ES2018)
fetchData()
    .then(data => console.log(data))
    .catch(error => console.error(error))
    .finally(() => {
        console.log('Cleanup');
        hideLoader();
    });

// Practical: loading indicator
showLoader();

fetchUser(1)
    .then(user => displayUser(user))
    .catch(error => showError(error))
    .finally(() => hideLoader());

// finally doesn't receive value
promise
    .then(value => console.log(value))
    .finally(() => {
        // No access to value or error
        console.log('Done');
    });

// Value passes through
Promise.resolve(42)
    .finally(() => console.log('Finally'))
    .then(value => console.log(value));  // 42

🔀 Promise Combinators

Promise.all()

// Wait for all promises to fulfill
let p1 = Promise.resolve(1);
let p2 = Promise.resolve(2);
let p3 = Promise.resolve(3);

Promise.all([p1, p2, p3])
    .then(values => {
        console.log(values);  // [1, 2, 3]
    });

// Parallel API calls
let usersPromise = fetch('/api/users').then(r => r.json());
let postsPromise = fetch('/api/posts').then(r => r.json());
let statsPromise = fetch('/api/stats').then(r => r.json());

Promise.all([usersPromise, postsPromise, statsPromise])
    .then(([users, posts, stats]) => {
        console.log('Users:', users);
        console.log('Posts:', posts);
        console.log('Stats:', stats);
    });

// If any rejects, all() rejects immediately
let p1 = Promise.resolve(1);
let p2 = Promise.reject('Error');
let p3 = Promise.resolve(3);

Promise.all([p1, p2, p3])
    .then(values => {
        console.log('Success:', values);  // Won't run
    })
    .catch(error => {
        console.error('Failed:', error);  // 'Error'
    });

// All must succeed for all() to succeed
Promise.all([
    fetch('/api/user'),
    fetch('/api/settings'),
    fetch('/api/preferences')
])
    .then(responses => Promise.all(responses.map(r => r.json())))
    .then(([user, settings, preferences]) => {
        console.log({ user, settings, preferences });
    })
    .catch(error => {
        console.error('One or more requests failed:', error);
    });

Promise.race()

// First to settle (fulfill or reject) wins
let p1 = new Promise(resolve => setTimeout(() => resolve(1), 1000));
let p2 = new Promise(resolve => setTimeout(() => resolve(2), 500));

Promise.race([p1, p2])
    .then(value => console.log(value));  // 2 (faster)

// Timeout pattern
function timeout(ms) {
    return new Promise((resolve, reject) => {
        setTimeout(() => reject('Timeout'), ms);
    });
}

Promise.race([
    fetch('/api/data'),
    timeout(5000)
])
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(error => console.error(error));  // 'Timeout' if slow

// First response wins
Promise.race([
    fetch('/api/server1/data'),
    fetch('/api/server2/data'),
    fetch('/api/server3/data')
])
    .then(response => response.json())
    .then(data => console.log('Got data from fastest server'));

Promise.allSettled()

// Wait for all to settle, never rejects (ES2020)
let p1 = Promise.resolve(1);
let p2 = Promise.reject('Error');
let p3 = Promise.resolve(3);

Promise.allSettled([p1, p2, p3])
    .then(results => {
        results.forEach(result => {
            if (result.status === 'fulfilled') {
                console.log('Success:', result.value);
            } else {
                console.log('Failed:', result.reason);
            }
        });
    });
// Output:
// Success: 1
// Failed: Error
// Success: 3

// Practical: multiple independent operations
let operations = [
    fetch('/api/users'),
    fetch('/api/posts'),
    fetch('/api/invalid')  // This fails
];

Promise.allSettled(operations)
    .then(results => {
        let successful = results.filter(r => r.status === 'fulfilled');
        let failed = results.filter(r => r.status === 'rejected');
        
        console.log(`${successful.length} succeeded`);
        console.log(`${failed.length} failed`);
        
        return successful.map(r => r.value);
    })
    .then(responses => Promise.all(responses.map(r => r.json())))
    .then(data => console.log('Available data:', data));

Promise.any()

// First to fulfill wins, ignores rejections (ES2021)
let p1 = Promise.reject('Error 1');
let p2 = new Promise(resolve => setTimeout(() => resolve(2), 500));
let p3 = Promise.reject('Error 2');

Promise.any([p1, p2, p3])
    .then(value => console.log(value));  // 2

// All reject = AggregateError
Promise.any([
    Promise.reject('Error 1'),
    Promise.reject('Error 2')
])
    .catch(error => {
        console.log(error instanceof AggregateError);  // true
        console.log(error.errors);  // ['Error 1', 'Error 2']
    });

// Practical: try multiple mirrors
Promise.any([
    fetch('https://mirror1.com/data.json'),
    fetch('https://mirror2.com/data.json'),
    fetch('https://mirror3.com/data.json')
])
    .then(response => response.json())
    .then(data => console.log('Got data from a mirror'))
    .catch(error => console.error('All mirrors failed'));

💡 Practical Examples

Sequential Operations

// Process items one by one
async function processSequentially(items) {
    let results = [];
    
    for (let item of items) {
        let result = await processItem(item);
        results.push(result);
    }
    
    return results;
}

// Or with reduce
function processSequentially(items) {
    return items.reduce((promise, item) => {
        return promise.then(results => {
            return processItem(item).then(result => {
                return [...results, result];
            });
        });
    }, Promise.resolve([]));
}

Retry Logic

function retry(fn, maxRetries = 3, delay = 1000) {
    return fn().catch(error => {
        if (maxRetries <= 0) {
            throw error;
        }
        
        return new Promise(resolve => {
            setTimeout(() => {
                resolve(retry(fn, maxRetries - 1, delay));
            }, delay);
        });
    });
}

// Usage
retry(() => fetch('/api/data'), 3, 1000)
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(error => console.error('Failed after retries'));

Timeout Wrapper

function withTimeout(promise, ms) {
    let timeout = new Promise((resolve, reject) => {
        setTimeout(() => reject(new Error('Timeout')), ms);
    });
    
    return Promise.race([promise, timeout]);
}

// Usage
withTimeout(fetch('/api/data'), 5000)
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(error => {
        if (error.message === 'Timeout') {
            console.error('Request timed out');
        } else {
            console.error('Request failed:', error);
        }
    });

Batch Processing

async function batchProcess(items, batchSize = 5) {
    let results = [];
    
    for (let i = 0; i < items.length; i += batchSize) {
        let batch = items.slice(i, i + batchSize);
        let batchResults = await Promise.all(
            batch.map(item => processItem(item))
        );
        results.push(...batchResults);
    }
    
    return results;
}

// Process 100 items in batches of 10
let items = Array.from({ length: 100 }, (_, i) => i);
batchProcess(items, 10)
    .then(results => console.log('All processed:', results.length));

Parallel Limit

async function parallelLimit(tasks, limit) {
    let results = [];
    let executing = [];
    
    for (let task of tasks) {
        let p = Promise.resolve().then(() => task());
        results.push(p);
        
        if (limit <= tasks.length) {
            let e = p.then(() => {
                executing.splice(executing.indexOf(e), 1);
            });
            executing.push(e);
            
            if (executing.length >= limit) {
                await Promise.race(executing);
            }
        }
    }
    
    return Promise.all(results);
}

// Max 3 concurrent requests
let tasks = urls.map(url => () => fetch(url));
parallelLimit(tasks, 3)
    .then(responses => console.log('All done'));

Cache with Promises

class PromiseCache {
    constructor() {
        this.cache = new Map();
    }
    
    get(key, fn) {
        if (this.cache.has(key)) {
            return this.cache.get(key);
        }
        
        let promise = fn().then(result => {
            this.cache.set(key, Promise.resolve(result));
            return result;
        }).catch(error => {
            this.cache.delete(key);
            throw error;
        });
        
        this.cache.set(key, promise);
        return promise;
    }
    
    clear() {
        this.cache.clear();
    }
}

// Usage
let cache = new PromiseCache();

cache.get('user:1', () => fetch('/api/users/1').then(r => r.json()))
    .then(user => console.log(user));

// Second call uses cached promise
cache.get('user:1', () => fetch('/api/users/1').then(r => r.json()))
    .then(user => console.log('From cache:', user));

Promise Pool

class PromisePool {
    constructor(concurrency) {
        this.concurrency = concurrency;
        this.running = 0;
        this.queue = [];
    }
    
    async add(fn) {
        while (this.running >= this.concurrency) {
            await Promise.race(this.queue);
        }
        
        this.running++;
        
        let promise = fn().finally(() => {
            this.running--;
            this.queue.splice(this.queue.indexOf(promise), 1);
        });
        
        this.queue.push(promise);
        return promise;
    }
}

// Usage
let pool = new PromisePool(3);  // Max 3 concurrent

for (let i = 0; i < 10; i++) {
    pool.add(() => {
        console.log('Starting task', i);
        return new Promise(resolve => {
            setTimeout(() => {
                console.log('Completed task', i);
                resolve(i);
            }, 1000);
        });
    });
}

⚠️ Common Mistakes

// Forgetting to return promise
function bad() {
    fetchData()
        .then(data => processData(data));  // Not returned!
}

bad().then(() => {
    console.log('Done?');  // Runs immediately, not after fetch
});

function good() {
    return fetchData()
        .then(data => processData(data));
}

// Nested promises (promise hell)
fetchUser(1)
    .then(user => {
        fetchPosts(user.id)
            .then(posts => {
                fetchComments(posts[0].id)
                    .then(comments => {
                        console.log(comments);
                    });
            });
    });

// Better: chain
fetchUser(1)
    .then(user => fetchPosts(user.id))
    .then(posts => fetchComments(posts[0].id))
    .then(comments => console.log(comments));

// Not handling errors
fetchData()
    .then(data => processData(data));  // No .catch()!

// Better
fetchData()
    .then(data => processData(data))
    .catch(error => console.error(error));

// Creating unnecessary promises
function unnecessary() {
    return new Promise(resolve => {
        fetchData()
            .then(data => resolve(data));
    });
}

function better() {
    return fetchData();  // Already returns promise
}

// Swallowing errors
promise.catch(error => {});  // Don't do this

// At least log
promise.catch(error => console.error(error));

🎯 Key Takeaways