Prerequisites
- Understanding of Promise<T> fundamentals ๐
- Basic async/await knowledge โก
- TypeScript generics experience ๐ป
What you'll learn
- Master Promise.race mechanics and use cases ๐ฏ
- Implement timeout patterns and competitive requests ๐๏ธ
- Handle race condition scenarios safely ๐
- Build resilient async applications with fallbacks โจ
๐ฏ Introduction
Welcome to the thrilling world of Promise racing! ๐ Imagine having multiple racers competing to bring you data - API endpoints, database queries, or file operations all racing to the finish line. Promise.race()
lets you work with whichever promise crosses the finish line first!
Whether youโre implementing timeout patterns โฐ, building fallback systems ๐ก๏ธ, or optimizing for the fastest response time ๐, Promise.race is your go-to tool for competitive async operations.
By the end of this tutorial, youโll be a Promise racing champion, able to orchestrate complex async scenarios where speed and resilience matter most. Letโs start your engines! ๐๏ธ
๐ Understanding Promise.race()
๐ค What is Promise.race()?
Think of Promise.race()
as a sprint competition ๐โโ๏ธ๐โโ๏ธ. You line up several runners (promises), fire the starting gun, and whoever finishes first wins the entire race - regardless of what happens to the other runners!
// ๐ Basic Promise.race example
const fastPromise = new Promise<string>((resolve) => {
setTimeout(() => resolve("I'm fast! ๐"), 100);
});
const slowPromise = new Promise<string>((resolve) => {
setTimeout(() => resolve("I'm slow... ๐"), 1000);
});
// ๐ Race them - fastest wins!
const winner = await Promise.race([fastPromise, slowPromise]);
console.log(winner); // "I'm fast! ๐" (after ~100ms)
๐ก Key Characteristics
- ๐ฅ First Wins All: The first settled promise (resolved OR rejected) determines the outcome
- ๐ฏ Type Safety: TypeScript preserves union types across all competing promises
- โก Immediate: Returns as soon as ANY promise settles
- ๐โโ๏ธ No Waiting: Doesnโt wait for slower promises to complete
// ๐จ TypeScript's type inference with Promise.race
const raceTypes = Promise.race([
Promise.resolve("string result"), // Promise<string>
Promise.resolve(42), // Promise<number>
Promise.resolve(true) // Promise<boolean>
]); // Type: Promise<string | number | boolean>
๐ง Basic Syntax and Usage
๐ Simple Racing Examples
Letโs explore the fundamentals:
// ๐ฎ Gaming scenario: Multiple save locations
interface SaveData {
playerId: string;
level: number;
score: number;
timestamp: Date;
}
class GameSaveService {
// ๐พ Save to local storage (fast)
private saveToLocal(data: SaveData): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => {
localStorage.setItem('gameData', JSON.stringify(data));
resolve("โ
Saved locally!");
}, 50); // Very fast
});
}
// โ๏ธ Save to cloud (slower but persistent)
private saveToCloud(data: SaveData): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => {
// Simulate cloud API call
resolve("โ๏ธ Saved to cloud!");
}, 300); // Slower
});
}
// ๐ฏ Save wherever is fastest!
async quickSave(data: SaveData): Promise<string> {
console.log("๐ Starting save race...");
const localSave = this.saveToLocal(data);
const cloudSave = this.saveToCloud(data);
// ๐ Race them - first one wins!
const result = await Promise.race([localSave, cloudSave]);
console.log(`๐ Winner: ${result}`);
return result; // TypeScript knows this is string
}
}
// ๐ฎ Using the game save service
async function demonstrateGameSave() {
const saveService = new GameSaveService();
const gameData: SaveData = {
playerId: "player123",
level: 42,
score: 98765,
timestamp: new Date()
};
const winner = await saveService.quickSave(gameData);
console.log(`๐ Save completed: ${winner}`);
}
๐ฏ Timeout Patterns
One of the most common uses of Promise.race is implementing timeouts:
// โฐ Timeout utility function
function withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => {
reject(new Error(`โฐ Operation timed out after ${timeoutMs}ms`));
}, timeoutMs);
});
return Promise.race([promise, timeoutPromise]);
}
// ๐ API service with timeout protection
class APIService {
// ๐ก Fetch user data with timeout
async fetchUserWithTimeout(userId: string): Promise<{ name: string; email: string }> {
const userPromise = fetch(`/api/users/${userId}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
});
// ๐ Race against timeout
return withTimeout(userPromise, 5000); // 5 second timeout
}
// ๐ Multiple endpoints race
async getFastestUserData(userId: string): Promise<any> {
const endpoints = [
`/api/v1/users/${userId}`, // Old API
`/api/v2/users/${userId}`, // New API
`/cdn/users/${userId}.json` // CDN cache
];
const promises = endpoints.map(endpoint =>
fetch(endpoint).then(res => res.json())
);
// ๐ First successful response wins
return Promise.race(promises);
}
}
๐ก Practical Examples
๐ Example 1: Smart Shopping Price Checker
Letโs build a price comparison service that gets the fastest price quote:
// ๐ช Store and pricing interfaces
interface Product {
id: string;
name: string;
emoji: string;
}
interface PriceQuote {
store: string;
price: number;
availability: boolean;
shippingTime: string;
storeEmoji: string;
}
interface PriceComparisonResult {
fastestQuote: PriceQuote;
allQuotes?: PriceQuote[];
responseTime: number;
}
class PriceComparison {
// ๐๏ธ Get quote from Amazon (slow but reliable)
private getAmazonPrice(product: Product): Promise<PriceQuote> {
return new Promise((resolve) => {
const delay = 800 + Math.random() * 400; // 800-1200ms
setTimeout(() => {
resolve({
store: "Amazon",
price: 29.99 + Math.random() * 10,
availability: true,
shippingTime: "2 days",
storeEmoji: "๐ฆ"
});
}, delay);
});
}
// ๐ฏ Get quote from Best Buy (medium speed)
private getBestBuyPrice(product: Product): Promise<PriceQuote> {
return new Promise((resolve) => {
const delay = 400 + Math.random() * 300; // 400-700ms
setTimeout(() => {
resolve({
store: "Best Buy",
price: 32.99 + Math.random() * 8,
availability: Math.random() > 0.2, // 80% chance available
shippingTime: "3-5 days",
storeEmoji: "๐ช"
});
}, delay);
});
}
// โก Get quote from local store (fast)
private getLocalPrice(product: Product): Promise<PriceQuote> {
return new Promise((resolve) => {
const delay = 150 + Math.random() * 200; // 150-350ms
setTimeout(() => {
resolve({
store: "Local Store",
price: 35.99 + Math.random() * 5,
availability: Math.random() > 0.3, // 70% chance available
shippingTime: "Same day",
storeEmoji: "๐ฌ"
});
}, delay);
});
}
// ๐ Get fastest price quote
async getFastestPrice(product: Product): Promise<PriceComparisonResult> {
console.log(`๐ Searching for best price on ${product.emoji} ${product.name}...`);
const startTime = Date.now();
const amazonQuote = this.getAmazonPrice(product);
const bestBuyQuote = this.getBestBuyPrice(product);
const localQuote = this.getLocalPrice(product);
try {
// ๐ Race for the fastest quote
const fastestQuote = await Promise.race([amazonQuote, bestBuyQuote, localQuote]);
const responseTime = Date.now() - startTime;
console.log(`โก Fastest response from ${fastestQuote.storeEmoji} ${fastestQuote.store} in ${responseTime}ms!`);
return {
fastestQuote,
responseTime
};
} catch (error) {
throw new Error(`๐ฅ All price checks failed: ${error.message}`);
}
}
// ๐ฏ Get fastest available product
async getFastestAvailablePrice(product: Product): Promise<PriceComparisonResult> {
console.log(`๐ฏ Finding fastest available price for ${product.emoji} ${product.name}...`);
const startTime = Date.now();
// ๐ Keep racing until we find an available item
const stores = [
() => this.getAmazonPrice(product),
() => this.getBestBuyPrice(product),
() => this.getLocalPrice(product)
];
for (let attempt = 0; attempt < 3; attempt++) {
try {
const promises = stores.map(storeFunc => storeFunc());
const quote = await Promise.race(promises);
if (quote.availability) {
const responseTime = Date.now() - startTime;
console.log(`โ
Found available item at ${quote.storeEmoji} ${quote.store}!`);
return { fastestQuote: quote, responseTime };
} else {
console.log(`โ ${quote.store} is out of stock, trying others...`);
}
} catch (error) {
console.log(`๐ Attempt ${attempt + 1} failed, retrying...`);
}
}
throw new Error("๐ No stores have this item available");
}
}
// ๐ฎ Demo the price comparison
async function demonstratePriceComparison() {
const priceChecker = new PriceComparison();
const product: Product = {
id: "ts-book-001",
name: "TypeScript Handbook",
emoji: "๐"
};
try {
// ๐ Race for fastest price
console.log("๐ Starting price race...\n");
const result = await priceChecker.getFastestPrice(product);
console.log(`\n๐ Winner: ${result.fastestQuote.storeEmoji} ${result.fastestQuote.store}`);
console.log(`๐ฐ Price: $${result.fastestQuote.price.toFixed(2)}`);
console.log(`๐ฆ Shipping: ${result.fastestQuote.shippingTime}`);
console.log(`โก Response time: ${result.responseTime}ms\n`);
// ๐ฏ Find fastest available
const availableResult = await priceChecker.getFastestAvailablePrice(product);
console.log(`\nโ
Fastest available option: ${availableResult.fastestQuote.storeEmoji} ${availableResult.fastestQuote.store}`);
} catch (error) {
console.error("๐ฅ Price comparison failed:", error.message);
}
}
๐ Example 2: Multi-Server Health Check
Letโs create a system that checks which server responds fastest:
// ๐ฅ๏ธ Server and health check interfaces
interface ServerInfo {
name: string;
url: string;
region: string;
emoji: string;
}
interface HealthCheckResult {
server: ServerInfo;
status: "healthy" | "degraded" | "down";
responseTime: number;
timestamp: Date;
}
interface FastestServerResult {
fastest: HealthCheckResult;
allResults?: HealthCheckResult[];
totalCheckTime: number;
}
class ServerHealthChecker {
private servers: ServerInfo[] = [
{ name: "US East", url: "https://api-east.example.com", region: "us-east-1", emoji: "๐บ๐ธ" },
{ name: "EU West", url: "https://api-eu.example.com", region: "eu-west-1", emoji: "๐ช๐บ" },
{ name: "Asia Pacific", url: "https://api-asia.example.com", region: "ap-southeast-1", emoji: "๐" },
{ name: "Australia", url: "https://api-au.example.com", region: "ap-southeast-2", emoji: "๐ฆ๐บ" }
];
// ๐ฅ Check single server health
private async checkServerHealth(server: ServerInfo): Promise<HealthCheckResult> {
const startTime = Date.now();
try {
// ๐ Simulate health check (replace with real fetch in production)
await new Promise((resolve, reject) => {
const responseTime = 50 + Math.random() * 500; // 50-550ms
const isHealthy = Math.random() > 0.1; // 90% success rate
setTimeout(() => {
if (isHealthy) {
resolve(undefined);
} else {
reject(new Error("Server timeout"));
}
}, responseTime);
});
const responseTime = Date.now() - startTime;
return {
server,
status: responseTime < 200 ? "healthy" : "degraded",
responseTime,
timestamp: new Date()
};
} catch (error) {
return {
server,
status: "down",
responseTime: Date.now() - startTime,
timestamp: new Date()
};
}
}
// ๐ Find fastest healthy server
async getFastestHealthyServer(): Promise<FastestServerResult> {
console.log("๐ Checking server health across all regions...");
const startTime = Date.now();
// ๐ Race all servers simultaneously
const healthChecks = this.servers.map(server => this.checkServerHealth(server));
try {
// ๐ First healthy server wins
const fastest = await Promise.race(healthChecks);
const totalCheckTime = Date.now() - startTime;
if (fastest.status === "down") {
throw new Error("Fastest server is down, checking others...");
}
console.log(`โก Fastest server: ${fastest.server.emoji} ${fastest.server.name} (${fastest.responseTime}ms)`);
return {
fastest,
totalCheckTime
};
} catch (error) {
console.log("๐ First server failed, waiting for others...");
// ๐ก๏ธ Fallback: wait for all results and pick best healthy one
const allResults = await Promise.allSettled(healthChecks);
const healthyServers = allResults
.filter((result): result is PromiseFulfilledResult<HealthCheckResult> =>
result.status === 'fulfilled' && result.value.status !== 'down'
)
.map(result => result.value)
.sort((a, b) => a.responseTime - b.responseTime);
if (healthyServers.length === 0) {
throw new Error("๐ All servers are down!");
}
const fastest = healthyServers[0];
const totalCheckTime = Date.now() - startTime;
console.log(`๐ก๏ธ Fallback server: ${fastest.server.emoji} ${fastest.server.name}`);
return {
fastest,
allResults: allResults.map(r => r.status === 'fulfilled' ? r.value : null).filter(Boolean) as HealthCheckResult[],
totalCheckTime
};
}
}
// ๐ Get comprehensive health report
async getHealthReport(): Promise<{ fastest: HealthCheckResult; all: HealthCheckResult[] }> {
console.log("๐ Generating comprehensive health report...");
const healthChecks = this.servers.map(server => this.checkServerHealth(server));
// ๐ Race for fastest + wait for all
const [fastest, ...allResults] = await Promise.all([
Promise.race(healthChecks),
...healthChecks
]);
return { fastest, all: allResults };
}
}
// ๐ฎ Demo the server health checker
async function demonstrateServerHealthCheck() {
const healthChecker = new ServerHealthChecker();
try {
// ๐ Find fastest server
console.log("๐ Racing servers for fastest response...\n");
const result = await healthChecker.getFastestHealthyServer();
console.log(`\n๐ Fastest healthy server:`);
console.log(` ${result.fastest.server.emoji} ${result.fastest.server.name}`);
console.log(` ๐ Region: ${result.fastest.server.region}`);
console.log(` โก Response: ${result.fastest.responseTime}ms`);
console.log(` ๐ Status: ${result.fastest.status}`);
console.log(` โฑ๏ธ Total check time: ${result.totalCheckTime}ms\n`);
// ๐ Get full report
console.log("๐ Getting full health report...");
const report = await healthChecker.getHealthReport();
console.log(`\n๐ All server results:`);
report.all.forEach(result => {
const statusEmoji = result.status === 'healthy' ? '๐' : result.status === 'degraded' ? '๐' : 'โค๏ธ';
console.log(` ${result.server.emoji} ${result.server.name}: ${statusEmoji} ${result.status} (${result.responseTime}ms)`);
});
} catch (error) {
console.error("๐ฅ Health check failed:", error.message);
}
}
๐ Advanced Concepts
๐งโโ๏ธ Competitive Loading with Fallbacks
Handle complex scenarios where you want the fastest result but need fallbacks:
// ๐ฏ Advanced race with intelligent fallbacks
class SmartDataLoader<T> {
// ๐ Race with fallback chain
async loadWithFallbacks<T>(
primary: () => Promise<T>,
fallbacks: (() => Promise<T>)[],
timeoutMs: number = 5000
): Promise<{ data: T; source: string; attemptCount: number }> {
// ๐ฏ Try primary first
try {
const primaryPromise = primary();
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error('Timeout')), timeoutMs);
});
const data = await Promise.race([primaryPromise, timeoutPromise]);
return { data, source: 'primary', attemptCount: 1 };
} catch (primaryError) {
console.log("๐ Primary failed, trying fallbacks...");
// ๐ก๏ธ Race all fallbacks
if (fallbacks.length === 0) {
throw new Error("No fallbacks available");
}
for (let i = 0; i < fallbacks.length; i++) {
try {
const fallbackPromises = fallbacks.slice(i).map(fallback => fallback());
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error('Fallback timeout')), timeoutMs);
});
const data = await Promise.race([...fallbackPromises, timeoutPromise]);
return { data, source: `fallback-${i + 1}`, attemptCount: i + 2 };
} catch (fallbackError) {
console.log(`๐ Fallback ${i + 1} failed...`);
continue;
}
}
throw new Error("All sources failed");
}
}
// ๐ฎ Smart cache-first loading
async smartLoad(
cacheLoader: () => Promise<T>,
networkLoader: () => Promise<T>,
maxCacheAge: number = 60000 // 1 minute
): Promise<{ data: T; source: 'cache' | 'network'; fresh: boolean }> {
const cachePromise = cacheLoader().then(data => ({ data, source: 'cache' as const }));
const networkPromise = networkLoader().then(data => ({ data, source: 'network' as const }));
try {
// ๐ Race cache vs network (cache usually wins)
const result = await Promise.race([cachePromise, networkPromise]);
if (result.source === 'cache') {
// ๐ Check if cache is fresh enough
const cacheTimestamp = Date.now(); // In real app, get from cache metadata
const isFresh = cacheTimestamp > Date.now() - maxCacheAge;
if (isFresh) {
console.log("โก Using fresh cache data");
return { ...result, fresh: true };
} else {
console.log("๐ Cache stale, waiting for network...");
const networkResult = await networkPromise;
return { ...networkResult, fresh: true };
}
}
console.log("๐ Using network data");
return { ...result, fresh: true };
} catch (error) {
throw new Error(`Smart load failed: ${error.message}`);
}
}
}
// ๐ฎ Using smart data loader
async function demonstrateSmartLoader() {
const loader = new SmartDataLoader<string>();
// ๐ Example: Loading user profile with fallbacks
const primaryLoader = () =>
new Promise<string>((resolve, reject) => {
setTimeout(() => {
Math.random() > 0.7 ? resolve("Primary data ๐ฏ") : reject(new Error("Primary failed"));
}, 200);
});
const fallbackLoaders = [
() => new Promise<string>((resolve) =>
setTimeout(() => resolve("Fallback 1 data ๐ก๏ธ"), 300)
),
() => new Promise<string>((resolve) =>
setTimeout(() => resolve("Fallback 2 data ๐"), 400)
)
];
try {
const result = await loader.loadWithFallbacks(primaryLoader, fallbackLoaders, 1000);
console.log(`โ
Loaded from ${result.source} in ${result.attemptCount} attempts: ${result.data}`);
} catch (error) {
console.error("๐ฅ All loading attempts failed:", error.message);
}
}
๐๏ธ Race with Progress Tracking
Track which promises are still racing:
// ๐ Race with detailed progress tracking
class ProgressiveRace<T> {
async raceWithProgress<T>(
promises: Promise<T>[],
onProgress?: (completed: number, total: number, result?: T) => void
): Promise<T> {
return new Promise((resolve, reject) => {
let completed = 0;
let resolved = false;
const total = promises.length;
promises.forEach((promise, index) => {
promise
.then(result => {
completed++;
onProgress?.(completed, total, result);
if (!resolved) {
resolved = true;
console.log(`๐ Promise ${index + 1} won the race!`);
resolve(result);
}
})
.catch(error => {
completed++;
onProgress?.(completed, total);
if (completed === total && !resolved) {
reject(new Error("All promises failed"));
}
});
});
});
}
}
// ๐ฎ Demo progressive race
async function demonstrateProgressiveRace() {
const progressRace = new ProgressiveRace();
const promises = [
new Promise<string>(resolve => setTimeout(() => resolve("Result A ๐
ฐ๏ธ"), 800)),
new Promise<string>(resolve => setTimeout(() => resolve("Result B ๐
ฑ๏ธ"), 300)),
new Promise<string>(resolve => setTimeout(() => resolve("Result C ๐"), 1200))
];
console.log("๐ Starting progressive race...");
try {
const winner = await progressRace.raceWithProgress(
promises,
(completed, total, result) => {
if (result) {
console.log(`๐ฏ First result: ${result}`);
}
console.log(`๐ Progress: ${completed}/${total} promises completed`);
}
);
console.log(`๐ Final winner: ${winner}`);
} catch (error) {
console.error("๐ฅ Race failed:", error.message);
}
}
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Ignoring Rejected Promises
// โ Dangerous - race can reject immediately!
async function badRace() {
const promises = [
Promise.reject(new Error("I fail fast! ๐ฅ")), // Rejects immediately
Promise.resolve("I'm slow but successful ๐") // Resolves later
];
// ๐ฅ This will throw immediately!
return Promise.race(promises);
}
// โ
Safe - handle rejections properly
async function safeRace() {
const promises = [
Promise.reject(new Error("I fail fast! ๐ฅ")).catch(() => null),
Promise.resolve("I'm slow but successful ๐")
];
const result = await Promise.race(promises);
return result || "No successful result";
}
// โ
Even better - filter failed promises
async function smartRace<T>(promises: Promise<T>[]): Promise<T> {
const wrappedPromises = promises.map(async (promise, index) => {
try {
const result = await promise;
return { success: true, result, index };
} catch (error) {
return { success: false, error, index };
}
});
const firstResult = await Promise.race(wrappedPromises);
if (firstResult.success) {
return firstResult.result;
} else {
throw new Error(`Promise ${firstResult.index} failed: ${firstResult.error.message}`);
}
}
๐คฏ Pitfall 2: Memory Leaks with Unfinished Promises
// โ Potential memory leak - other promises keep running
async function leakyRace() {
const promises = [
fetch('/fast-api'), // Finishes first
fetch('/slow-api'), // Still running in background!
fetch('/very-slow-api') // Still running in background!
];
return Promise.race(promises); // Other fetches continue consuming resources
}
// โ
Clean up with AbortController
async function cleanRace() {
const controller = new AbortController();
const promises = [
fetch('/fast-api', { signal: controller.signal }),
fetch('/slow-api', { signal: controller.signal }),
fetch('/very-slow-api', { signal: controller.signal })
];
try {
const result = await Promise.race(promises);
controller.abort(); // ๐งน Cancel other requests
return result;
} catch (error) {
controller.abort(); // ๐งน Clean up on error too
throw error;
}
}
๐ฏ Pitfall 3: Wrong Type Expectations
// โ Wrong - assuming all promises have same type
function badTyping() {
const promises = [
Promise.resolve("string"),
Promise.resolve(42),
Promise.resolve(true)
];
// ๐ซ TypeScript error - result could be string | number | boolean
return Promise.race(promises).then((result: string) => result.toUpperCase());
}
// โ
Correct - handle union types properly
function goodTyping() {
const promises = [
Promise.resolve("string"),
Promise.resolve(42),
Promise.resolve(true)
];
return Promise.race(promises).then(result => {
if (typeof result === 'string') {
return result.toUpperCase(); // โ
TypeScript knows this is safe
}
return String(result).toUpperCase(); // ๐ฏ Convert others to string
});
}
๐ ๏ธ Best Practices
- ๐ก๏ธ Always Handle Rejections: First settled promise (even failures) wins the race
- ๐งน Clean Up Resources: Use AbortController or cleanup logic for unfinished promises
- ๐ฏ Type Union Handling: Expect union types when racing different promise types
- โฐ Implement Timeouts: Use race for timeout patterns with actual operations
- ๐ Plan for Failures: Have fallback strategies when fastest promise fails
- ๐ Consider All Results: Sometimes you need the fastest + all results for comparison
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Multi-Source Data Aggregator
Create a system that races multiple data sources but falls back intelligently:
๐ Requirements:
- ๐ Race multiple API endpoints for fastest response
- โฐ Implement timeout protection (2 seconds)
- ๐ก๏ธ Fall back to cached data if all APIs fail
- ๐ Track response times and success rates
- ๐ Retry failed sources with exponential backoff
- ๐งน Clean up ongoing requests when one wins
๐ Bonus Points:
- Add health monitoring for each source
- Implement adaptive timeout based on historical performance
- Create a load balancer that prefers faster sources
- Add real-time performance metrics dashboard
๐ก Solution
๐ Click to see solution
// ๐๏ธ Multi-source data aggregator with intelligent racing
interface DataSource {
name: string;
url: string;
priority: number;
emoji: string;
}
interface SourceResult<T> {
data: T;
source: DataSource;
responseTime: number;
fromCache: boolean;
}
interface SourceMetrics {
totalRequests: number;
successCount: number;
avgResponseTime: number;
lastError?: string;
}
class MultiSourceAggregator<T> {
private metrics = new Map<string, SourceMetrics>();
private cache = new Map<string, { data: T; timestamp: number }>();
private activeControllers = new Set<AbortController>();
constructor(
private sources: DataSource[],
private cacheMaxAge: number = 60000 // 1 minute
) {
// ๐ Initialize metrics
sources.forEach(source => {
this.metrics.set(source.name, {
totalRequests: 0,
successCount: 0,
avgResponseTime: 0
});
});
}
// ๐ Race all sources with intelligent fallbacks
async fetchData(endpoint: string, timeoutMs: number = 2000): Promise<SourceResult<T>> {
console.log(`๐ Racing ${this.sources.length} sources for: ${endpoint}`);
// ๐งน Clean up any previous requests
this.cleanupActiveRequests();
const racePromises = this.sources.map(source =>
this.fetchFromSource(source, endpoint, timeoutMs)
);
try {
// ๐ Race all sources
const result = await Promise.race(racePromises);
console.log(`โก Winner: ${result.source.emoji} ${result.source.name} (${result.responseTime}ms)`);
// ๐งน Cancel other requests
this.cleanupActiveRequests();
// ๐พ Cache the result
this.cacheResult(endpoint, result.data);
return result;
} catch (error) {
console.log("๐ All sources failed, checking cache...");
// ๐ก๏ธ Fallback to cache
const cachedData = this.getCachedData(endpoint);
if (cachedData) {
console.log("๐พ Using cached data as fallback");
return {
data: cachedData,
source: { name: "Cache", url: "local", priority: 0, emoji: "๐พ" },
responseTime: 0,
fromCache: true
};
}
throw new Error(`All sources failed and no cache available: ${error.message}`);
}
}
// ๐ Fetch from a single source
private async fetchFromSource(
source: DataSource,
endpoint: string,
timeoutMs: number
): Promise<SourceResult<T>> {
const controller = new AbortController();
this.activeControllers.add(controller);
const startTime = Date.now();
const metrics = this.metrics.get(source.name)!;
metrics.totalRequests++;
try {
// ๐ Simulate API call (replace with real fetch)
const data = await this.simulateAPICall(source, endpoint, controller.signal, timeoutMs);
const responseTime = Date.now() - startTime;
// ๐ Update metrics
metrics.successCount++;
metrics.avgResponseTime = (metrics.avgResponseTime + responseTime) / 2;
this.activeControllers.delete(controller);
return {
data,
source,
responseTime,
fromCache: false
};
} catch (error) {
const responseTime = Date.now() - startTime;
metrics.lastError = error.message;
this.activeControllers.delete(controller);
throw new Error(`${source.emoji} ${source.name} failed (${responseTime}ms): ${error.message}`);
}
}
// ๐ฒ Simulate API call (replace with real implementation)
private simulateAPICall(
source: DataSource,
endpoint: string,
signal: AbortSignal,
timeoutMs: number
): Promise<T> {
return new Promise((resolve, reject) => {
// ๐ฏ Simulate different response times based on priority
const baseDelay = (6 - source.priority) * 200; // Higher priority = lower delay
const randomDelay = Math.random() * 500;
const totalDelay = baseDelay + randomDelay;
const timeoutId = setTimeout(() => {
if (signal.aborted) {
reject(new Error("Request aborted"));
return;
}
// ๐ฒ 90% success rate
if (Math.random() > 0.1) {
resolve({
message: `Data from ${source.name}`,
timestamp: new Date(),
endpoint,
source: source.name
} as T);
} else {
reject(new Error("Simulated API failure"));
}
}, Math.min(totalDelay, timeoutMs));
// ๐งน Handle abort
signal.addEventListener('abort', () => {
clearTimeout(timeoutId);
reject(new Error("Request aborted"));
});
// โฐ Handle timeout
setTimeout(() => {
if (!signal.aborted) {
reject(new Error("Request timeout"));
}
}, timeoutMs);
});
}
// ๐พ Cache management
private cacheResult(key: string, data: T): void {
this.cache.set(key, {
data,
timestamp: Date.now()
});
}
private getCachedData(key: string): T | null {
const cached = this.cache.get(key);
if (!cached) return null;
const age = Date.now() - cached.timestamp;
if (age > this.cacheMaxAge) {
this.cache.delete(key);
return null;
}
return cached.data;
}
// ๐งน Cleanup active requests
private cleanupActiveRequests(): void {
this.activeControllers.forEach(controller => {
controller.abort();
});
this.activeControllers.clear();
}
// ๐ Get performance metrics
getMetrics(): Map<string, SourceMetrics> {
return new Map(this.metrics);
}
// ๐ฏ Get best performing source
getBestSource(): DataSource | null {
let bestSource: DataSource | null = null;
let bestScore = -1;
for (const source of this.sources) {
const metrics = this.metrics.get(source.name);
if (!metrics || metrics.totalRequests === 0) continue;
const successRate = metrics.successCount / metrics.totalRequests;
const avgTime = metrics.avgResponseTime || 1000;
// ๐ Score = success rate / avg response time
const score = successRate / (avgTime / 1000);
if (score > bestScore) {
bestScore = score;
bestSource = source;
}
}
return bestSource;
}
}
// ๐ฎ Demo the multi-source aggregator
async function demonstrateMultiSourceAggregator() {
const sources: DataSource[] = [
{ name: "Primary API", url: "https://api.example.com", priority: 5, emoji: "๐ฏ" },
{ name: "Backup API", url: "https://backup.example.com", priority: 3, emoji: "๐ก๏ธ" },
{ name: "CDN", url: "https://cdn.example.com", priority: 4, emoji: "โก" },
{ name: "Edge Cache", url: "https://edge.example.com", priority: 2, emoji: "๐" }
];
const aggregator = new MultiSourceAggregator<any>(sources, 30000);
try {
// ๐ Test multiple requests
for (let i = 1; i <= 3; i++) {
console.log(`\n๐ Request ${i}:`);
const result = await aggregator.fetchData(`/api/data/${i}`, 1500);
console.log(`โ
Success: ${result.source.emoji} ${result.source.name}`);
console.log(`๐ Response time: ${result.responseTime}ms`);
console.log(`๐พ From cache: ${result.fromCache ? 'Yes' : 'No'}`);
}
// ๐ Show performance metrics
console.log(`\n๐ Performance Metrics:`);
const metrics = aggregator.getMetrics();
metrics.forEach((metric, sourceName) => {
const successRate = metric.totalRequests > 0
? (metric.successCount / metric.totalRequests * 100).toFixed(1)
: '0';
console.log(` ${sourceName}:`);
console.log(` ๐ Success rate: ${successRate}%`);
console.log(` โฑ๏ธ Avg response: ${metric.avgResponseTime.toFixed(0)}ms`);
console.log(` ๐ข Total requests: ${metric.totalRequests}`);
});
// ๐ Show best performing source
const bestSource = aggregator.getBestSource();
if (bestSource) {
console.log(`\n๐ Best performing source: ${bestSource.emoji} ${bestSource.name}`);
}
} catch (error) {
console.error("๐ฅ Aggregator demo failed:", error.message);
}
}
// ๐ Run the demo
demonstrateMultiSourceAggregator();
๐ Key Takeaways
Youโve mastered Promise.race! Hereโs what you can now do:
- โ Race promises effectively for fastest responses and timeouts ๐ช
- โ Handle rejection scenarios and implement smart fallbacks ๐ก๏ธ
- โ Build competitive async systems with proper cleanup ๐ฏ
- โ Use advanced patterns like progressive racing and metrics ๐
- โ Avoid common pitfalls like memory leaks and type issues ๐
Remember: Promise.race is perfect when you need the fastest result, but always plan for what happens when the winner fails! ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered Promise.race in TypeScript!
Hereโs what to explore next:
- ๐ป Practice with the multi-source aggregator exercise above
- ๐๏ธ Build a real-time performance monitoring system
- ๐ Move on to our next tutorial: โPromise.allSettled: Handling Mixed Resultsโ
- ๐ Explore advanced patterns like Promise scheduling and queuing
Remember: The fastest doesnโt always win - but with proper fallbacks, youโll always have a winner! ๐
Happy coding! ๐๐โจ