Back to Blog
JavaScriptPromisesAsyncFundamentals

JavaScript Promises Deep Dive

Master JavaScript Promises. From basics to error handling to advanced patterns and composition.

B
Bootspring Team
Engineering
June 9, 2021
6 min read

Promises handle asynchronous operations elegantly. Here's everything you need to know.

Promise Basics#

1// Creating a promise 2const promise = new Promise((resolve, reject) => { 3 // Async operation 4 setTimeout(() => { 5 const success = true; 6 7 if (success) { 8 resolve('Operation completed'); 9 } else { 10 reject(new Error('Operation failed')); 11 } 12 }, 1000); 13}); 14 15// Consuming a promise 16promise 17 .then(result => { 18 console.log(result); 19 }) 20 .catch(error => { 21 console.error(error); 22 }) 23 .finally(() => { 24 console.log('Cleanup'); 25 }); 26 27// Promise states 28// Pending: initial state 29// Fulfilled: resolved successfully 30// Rejected: operation failed

Promise Static Methods#

1// Promise.resolve - create resolved promise 2const resolved = Promise.resolve('value'); 3resolved.then(v => console.log(v)); // 'value' 4 5// Promise.reject - create rejected promise 6const rejected = Promise.reject(new Error('error')); 7rejected.catch(e => console.error(e)); 8 9// Promise.all - wait for all, fail if any fails 10const results = await Promise.all([ 11 fetch('/api/users'), 12 fetch('/api/posts'), 13 fetch('/api/comments'), 14]); 15// All responses or first rejection 16 17// Promise.allSettled - wait for all, never rejects 18const results = await Promise.allSettled([ 19 fetch('/api/users'), 20 fetch('/api/posts'), 21 Promise.reject('error'), 22]); 23// [ 24// { status: 'fulfilled', value: Response }, 25// { status: 'fulfilled', value: Response }, 26// { status: 'rejected', reason: 'error' } 27// ] 28 29// Promise.race - first to settle (resolve or reject) 30const first = await Promise.race([ 31 fetch('/api/slow'), 32 timeout(5000), 33]); 34 35// Promise.any - first to resolve (ignores rejections) 36const fastest = await Promise.any([ 37 fetch('https://cdn1.example.com/resource'), 38 fetch('https://cdn2.example.com/resource'), 39 fetch('https://cdn3.example.com/resource'), 40]); 41// First successful response

Chaining#

1// Each then returns a new promise 2fetchUser(userId) 3 .then(user => fetchUserPosts(user.id)) 4 .then(posts => fetchPostComments(posts[0].id)) 5 .then(comments => { 6 console.log(comments); 7 }) 8 .catch(error => { 9 console.error('Error in chain:', error); 10 }); 11 12// Return values are wrapped in Promise.resolve 13Promise.resolve(1) 14 .then(x => x + 1) // Returns 2 15 .then(x => x * 2) // Returns 4 16 .then(x => { 17 throw new Error('Oops'); 18 }) 19 .then(x => x + 1) // Skipped 20 .catch(e => 0) // Returns 0 21 .then(x => x + 10) // Returns 10 22 .then(console.log); // Logs 10 23 24// Returning promises in chain 25fetchUser(1) 26 .then(user => { 27 // Returning a promise 28 return fetchProfile(user.id); 29 }) 30 .then(profile => { 31 // profile is the resolved value 32 console.log(profile); 33 });

Error Handling#

1// Catch handles rejections 2fetchData() 3 .then(data => processData(data)) 4 .catch(error => { 5 console.error('Error:', error); 6 return defaultValue; // Recovery 7 }) 8 .then(result => { 9 // Continues with recovered value or processed data 10 }); 11 12// Multiple catch blocks 13fetchData() 14 .then(data => { 15 if (!data) throw new ValidationError('No data'); 16 return processData(data); 17 }) 18 .catch(error => { 19 if (error instanceof ValidationError) { 20 return handleValidationError(error); 21 } 22 throw error; // Re-throw other errors 23 }) 24 .catch(error => { 25 // Handles re-thrown and other errors 26 console.error('Unhandled:', error); 27 }); 28 29// Error boundaries with Promise.all 30async function fetchAllSafe(urls) { 31 const results = await Promise.allSettled( 32 urls.map(url => fetch(url).then(r => r.json())) 33 ); 34 35 return results.map(result => { 36 if (result.status === 'fulfilled') { 37 return { success: true, data: result.value }; 38 } 39 return { success: false, error: result.reason }; 40 }); 41}

Async/Await#

1// Async functions return promises 2async function fetchUserData(userId) { 3 const response = await fetch(`/api/users/${userId}`); 4 const user = await response.json(); 5 return user; 6} 7 8// Error handling with try/catch 9async function safeOperation() { 10 try { 11 const result = await riskyOperation(); 12 return result; 13 } catch (error) { 14 console.error('Operation failed:', error); 15 return defaultValue; 16 } finally { 17 cleanup(); 18 } 19} 20 21// Parallel execution 22async function fetchAllParallel() { 23 // Sequential (slow) 24 const users = await fetchUsers(); 25 const posts = await fetchPosts(); 26 27 // Parallel (fast) 28 const [users, posts] = await Promise.all([ 29 fetchUsers(), 30 fetchPosts(), 31 ]); 32 33 return { users, posts }; 34} 35 36// Awaiting multiple with error handling 37async function fetchWithFallbacks() { 38 const results = await Promise.allSettled([ 39 fetchPrimary(), 40 fetchFallback(), 41 ]); 42 43 const successful = results 44 .filter(r => r.status === 'fulfilled') 45 .map(r => r.value); 46 47 return successful[0] || null; 48}

Advanced Patterns#

1// Timeout wrapper 2function withTimeout(promise, ms) { 3 const timeout = new Promise((_, reject) => { 4 setTimeout(() => reject(new Error('Timeout')), ms); 5 }); 6 7 return Promise.race([promise, timeout]); 8} 9 10await withTimeout(fetch('/api/slow'), 5000); 11 12// Retry with exponential backoff 13async function retry(fn, retries = 3, delay = 1000) { 14 for (let i = 0; i < retries; i++) { 15 try { 16 return await fn(); 17 } catch (error) { 18 if (i === retries - 1) throw error; 19 await new Promise(r => setTimeout(r, delay * Math.pow(2, i))); 20 } 21 } 22} 23 24await retry(() => fetch('/api/flaky'), 3, 1000); 25 26// Deferred promise 27function createDeferred() { 28 let resolve, reject; 29 const promise = new Promise((res, rej) => { 30 resolve = res; 31 reject = rej; 32 }); 33 return { promise, resolve, reject }; 34} 35 36const deferred = createDeferred(); 37// Later... 38deferred.resolve('value'); 39await deferred.promise; 40 41// Serial execution 42async function serial(tasks) { 43 const results = []; 44 for (const task of tasks) { 45 results.push(await task()); 46 } 47 return results; 48} 49 50// Concurrent with limit 51async function concurrent(tasks, limit) { 52 const results = []; 53 const executing = new Set(); 54 55 for (const task of tasks) { 56 const promise = task().then(result => { 57 executing.delete(promise); 58 return result; 59 }); 60 61 executing.add(promise); 62 results.push(promise); 63 64 if (executing.size >= limit) { 65 await Promise.race(executing); 66 } 67 } 68 69 return Promise.all(results); 70}

Promise Queue#

1class PromiseQueue { 2 constructor(concurrency = 1) { 3 this.concurrency = concurrency; 4 this.pending = []; 5 this.running = 0; 6 } 7 8 add(fn) { 9 return new Promise((resolve, reject) => { 10 this.pending.push({ fn, resolve, reject }); 11 this.process(); 12 }); 13 } 14 15 process() { 16 while (this.running < this.concurrency && this.pending.length > 0) { 17 const { fn, resolve, reject } = this.pending.shift(); 18 this.running++; 19 20 fn() 21 .then(resolve) 22 .catch(reject) 23 .finally(() => { 24 this.running--; 25 this.process(); 26 }); 27 } 28 } 29} 30 31const queue = new PromiseQueue(3); 32 33const results = await Promise.all([ 34 queue.add(() => fetch('/api/1')), 35 queue.add(() => fetch('/api/2')), 36 queue.add(() => fetch('/api/3')), 37 queue.add(() => fetch('/api/4')), 38 queue.add(() => fetch('/api/5')), 39]); 40// Max 3 concurrent requests

Common Patterns#

1// Caching promise results 2const cache = new Map(); 3 4async function fetchCached(url) { 5 if (cache.has(url)) { 6 return cache.get(url); 7 } 8 9 const promise = fetch(url).then(r => r.json()); 10 cache.set(url, promise); 11 return promise; 12} 13 14// Debounced promise 15function debouncePromise(fn, delay) { 16 let timeout; 17 let pendingPromise; 18 19 return function(...args) { 20 clearTimeout(timeout); 21 22 return new Promise((resolve, reject) => { 23 timeout = setTimeout(async () => { 24 try { 25 const result = await fn.apply(this, args); 26 resolve(result); 27 } catch (error) { 28 reject(error); 29 } 30 }, delay); 31 }); 32 }; 33} 34 35const debouncedSearch = debouncePromise(searchAPI, 300); 36 37// Promise memoization 38function memoizeAsync(fn) { 39 const cache = new Map(); 40 41 return async function(...args) { 42 const key = JSON.stringify(args); 43 44 if (cache.has(key)) { 45 return cache.get(key); 46 } 47 48 const result = await fn.apply(this, args); 49 cache.set(key, result); 50 return result; 51 }; 52}

Best Practices#

Error Handling: ✓ Always handle rejections ✓ Use try/catch with async/await ✓ Provide meaningful error messages ✓ Consider error recovery strategies Performance: ✓ Use Promise.all for parallel execution ✓ Avoid unnecessary sequential awaits ✓ Implement timeouts for network calls ✓ Consider request caching Patterns: ✓ Use Promise.allSettled for mixed results ✓ Implement retry logic for flaky operations ✓ Use queues to limit concurrency ✓ Cancel unnecessary promises when possible

Conclusion#

Promises provide powerful abstractions for async code. Use Promise.all for parallel operations, implement proper error handling, and leverage async/await for cleaner syntax. Master patterns like retry, timeout, and queue for robust applications.

Share this article

Help spread the word about Bootspring