Promises are central to modern JavaScript, but most developers use only the basics. Firefox fully implements the Promise specification and the full Promise static API. This guide covers practical patterns: error handling, concurrency control, cancellation, and the less-used Promise methods.
Promise Fundamentals Recap
// Creating a Promise
const p = new Promise((resolve, reject) => {
// Async operation
setTimeout(() => {
if (success) resolve(value);
else reject(new Error('failed'));
}, 1000);
});
// Consuming
p.then(value => console.log(value))
.catch(err => console.error(err))
.finally(() => cleanup());
// Async/await style (same thing, nicer syntax)
try {
const value = await p;
} catch (err) {
console.error(err);
} finally {
cleanup();
}
Promise.all vs Promise.allSettled vs Promise.any vs Promise.race
const p1 = fetch('/api/users');
const p2 = fetch('/api/posts');
const p3 = fetch('/api/comments');
// Promise.all: wait for ALL to succeed, fail fast on any rejection
// Returns: array of results in same order
// Use when: all requests MUST succeed
const [users, posts, comments] = await Promise.all([p1, p2, p3]);
// Promise.allSettled: wait for ALL to settle (succeed or fail)
// Returns: array of {status: 'fulfilled'|'rejected', value|reason}
// Use when: some can fail, you want all results
const results = await Promise.allSettled([p1, p2, p3]);
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log('Success:', result.value);
} else {
console.error('Failed:', result.reason);
}
});
// Promise.any: first to SUCCEED wins, fails only if ALL fail
// Returns: first fulfilled value
// Use when: trying multiple sources, use first available
const fastResponse = await Promise.any([
fetch('https://cdn1.example.com/data.json'),
fetch('https://cdn2.example.com/data.json'),
fetch('https://cdn3.example.com/data.json')
]);
// Promise.race: first to settle (success OR failure) wins
// Returns: first settled value/reason
// Use when: timeout patterns
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), 5000)
);
const result = await Promise.race([fetch('/api/slow'), timeout]);
Controlled Concurrency
Running 20 requests simultaneously is usually a bad idea. Control concurrency:
async function mapWithConcurrency(items, asyncFn, concurrency = 5) {
const results = new Array(items.length);
let index = 0;
async function worker() {
while (index < items.length) {
const currentIndex = index++;
results[currentIndex] = await asyncFn(items[currentIndex], currentIndex);
}
}
// Run 'concurrency' workers in parallel
await Promise.all(
Array.from({ length: Math.min(concurrency, items.length) }, worker)
);
return results;
}
// Process 100 items, max 5 at a time
const processed = await mapWithConcurrency(items, async (item) => {
return await processItem(item);
}, 5);
Error Handling Patterns
// Pattern 1: Centralized error handling
async function safeRequest(url) {
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return [await response.json(), null]; // [data, error]
} catch (err) {
return [null, err]; // [data, error]
}
}
// Usage: no try/catch at call sites
const [user, error] = await safeRequest('/api/user/123');
if (error) { handleError(error); return; }
console.log(user);
// Pattern 2: Promise utilities
function withTimeout(promise, ms) {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms)
)
]);
}
function withRetry(fn, attempts = 3, delay = 1000) {
return async function(...args) {
for (let i = 0; i < attempts; i++) {
try {
return await fn(...args);
} catch (err) {
if (i === attempts - 1) throw err;
await new Promise(resolve => setTimeout(resolve, delay * Math.pow(2, i)));
}
}
};
}
const fetchWithRetry = withRetry(fetch, 3, 500);
const data = await fetchWithRetry('/api/unreliable');
Cancellable Promises with AbortController
Promises themselves aren't cancellable, but you can pass a signal:
function cancellableDelay(ms, signal) {
return new Promise((resolve, reject) => {
if (signal?.aborted) { reject(new DOMException('Aborted', 'AbortError')); return; }
const timer = setTimeout(resolve, ms);
signal?.addEventListener('abort', () => {
clearTimeout(timer);
reject(new DOMException('Aborted', 'AbortError'));
});
});
}
// Usage
const controller = new AbortController();
cancellableDelay(5000, controller.signal)
.then(() => console.log('delay complete'))
.catch(err => {
if (err.name === 'AbortError') console.log('cancelled');
});
controller.abort(); // Cancel the delay
Promise.withResolvers (Firefox 119+)
A new static method that returns { promise, resolve, reject }:
// Old pattern (verbose)
let resolve, reject;
const promise = new Promise((res, rej) => { resolve = res; reject = rej; });
// New pattern
const { promise, resolve, reject } = Promise.withResolvers();
// Useful for: passing resolve/reject to other code
class TaskQueue {
constructor() {
this.queue = [];
}
add(task) {
const { promise, resolve, reject } = Promise.withResolvers();
this.queue.push({ task, resolve, reject });
this.processNext();
return promise;
}
async processNext() {
if (this.queue.length === 0) return;
const { task, resolve, reject } = this.queue.shift();
try {
resolve(await task());
} catch (err) {
reject(err);
}
this.processNext();
}
}
Deduplicating Concurrent Requests
When multiple parts of your app might request the same data simultaneously:
const inFlight = new Map();
async function deduplicateFetch(url) {
if (inFlight.has(url)) {
return inFlight.get(url); // Return the same promise
}
const promise = fetch(url)
.then(r => r.json())
.finally(() => inFlight.delete(url));
inFlight.set(url, promise);
return promise;
}
// All these calls get the SAME promise (one network request)
const [a, b, c] = await Promise.all([
deduplicateFetch('/api/config'),
deduplicateFetch('/api/config'),
deduplicateFetch('/api/config')
]);
Sequential vs. Parallel Execution
// SEQUENTIAL (each waits for previous): 3 seconds total
const user = await fetch('/api/user');
const posts = await fetch(`/api/posts?userId=${user.id}`);
// PARALLEL (all run simultaneously): ~1 second total
const [user, topPosts] = await Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/top-posts').then(r => r.json())
]);
// When to use sequential: when result of one request is needed for the next
// When to use parallel: when requests are independent
Testing Promises in Firefox DevTools
Firefox's DevTools console supports top-level await for testing:
// In console:
const data = await fetch('/api/users').then(r => r.json());
data.map(u => u.name)
The console shows the resolved value of awaited promises directly, without .then() handlers.
Top comments (0)