JavaScript
Aggregating Stock Market Data: A JavaScript Promises Tutorial
Introduction
Asynchronous operations are unavoidable in JavaScript. Fetching prices from a market data provider, querying a database, reading a file — these operations take time, and the program must keep working while they complete. The traditional answer was callbacks: pass a function and the runtime will call it when the operation finishes. For one operation, that is fine. For three sequential operations where each depends on the previous one's result, callbacks nest three levels deep. For an error handler on each level, the nesting doubles. The code structure stops reflecting what the program does and starts reflecting the execution machinery.
Promises, introduced in ES6 and adopted into the JavaScript standard, solve this by representing an asynchronous value as an object. A Promise is either pending, fulfilled with a value, or rejected with a reason. The .then() and .catch() methods attach callbacks to those states, and they return new promises — so chains are flat instead of nested. Promise.all runs multiple operations in parallel and waits for all of them; Promise.allSettled does the same but does not stop on the first failure.
This tutorial builds a market data aggregation service that fetches prices, computes positions, and generates portfolio summaries. Each section introduces one Promise capability — chaining, parallel execution, partial failure handling, and async/await syntax — using the aggregator's real requirements to motivate each tool.
Background
A Promise is an object with three possible states:
- Pending: the operation has not completed yet
- Fulfilled: the operation succeeded and the promise has a value
- Rejected: the operation failed and the promise has a reason (an error)
Key methods:
.then(onFulfilled, onRejected): attaches callbacks to both states; returns a new promise resolved with the callback's return value.catch(onRejected): shorthand for.then(null, onRejected); handles rejections.finally(fn): runsfnregardless of outcome; does not change the promise's valuePromise.all(iterable): takes an array of promises; resolves when all resolve, rejects immediately when any one rejectsPromise.allSettled(iterable): takes an array of promises; always resolves with an array of result objects, each with astatusof"fulfilled"or"rejected"Promise.race(iterable): resolves or rejects as soon as the first promise settles
async and await are syntactic sugar over promises. An async function always returns a Promise. await pauses execution of the async function until the awaited Promise settles, then resumes with the resolved value or throws the rejection reason.
Practical Scenario
A portfolio management platform fetches real-time price data from three provider APIs: a primary exchange feed, a secondary fallback, and a delay-tolerant bulk data service. Each fetch is independent and can be issued in parallel. The platform computes each position's current value by multiplying shares held by the current price and aggregates results across the portfolio.
During peak market hours, some provider APIs respond slowly or return errors for specific tickers. The platform must not fail the entire portfolio update when one ticker's price fetch fails — the five tickers that succeeded should update immediately while the failed ticker is flagged for retry. The platform also implements a timeout: if any single fetch has not responded within two seconds, it is treated as a failure rather than blocking the entire update.
Implementing this correctly with callbacks requires coordinating multiple nested error paths and a manual counter to detect when all parallel fetches have completed. Missing the counter by one means the summary is never emitted. Getting the error path wrong means a single API failure silently prevents the entire portfolio from updating.
The Problem
Create the initial aggregator:
touch market.js
Run it using:
node market.js
function fetchPrice(ticker, callback) {
setTimeout(() => {
const prices = { AAPL: 182.30, MSFT: 415.10, NVDA: 875.50, TSLA: 172.80 };
if (!prices[ticker]) {
callback(new Error(`Unknown ticker: ${ticker}`));
} else {
callback(null, { ticker, price: prices[ticker] });
}
}, Math.random() * 100);
}
function getPortfolio(tickers, callback) {
const results = [];
let completed = 0;
let failed = false;
tickers.forEach(ticker => {
fetchPrice(ticker, (err, data) => {
if (failed) return;
if (err) {
failed = true;
return callback(err);
}
results.push(data);
completed++;
if (completed === tickers.length) {
callback(null, results);
}
});
});
}
getPortfolio(["AAPL", "MSFT", "NVDA"], (err, results) => {
if (err) {
console.error("Portfolio fetch failed:", err.message);
return;
}
results.forEach(r => console.log(`${r.ticker}: $${r.price}`));
});
AAPL: $182.3
NVDA: $875.5
MSFT: $415.1
This implementation has four problems that are invisible in the happy path. The completed counter is an error-prone manual coordination mechanism. If tickers is empty, callback is never called. If two fetches fail simultaneously, callback is called twice. Adding a timeout for slow fetches requires a setTimeout wrapping each fetch, a second clearTimeout call for success, and more counter logic — the complexity scales with requirements rather than remaining flat.
Creating and Using Promises
A Promise wraps an asynchronous operation that calls resolve on success and reject on failure. The .then() method receives the resolved value; .catch() receives the rejection reason.
Replace the entire content of market.js with the following:
function fetchPrice(ticker) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const prices = { AAPL: 182.30, MSFT: 415.10, NVDA: 875.50, TSLA: 172.80 };
if (!prices[ticker]) {
reject(new Error(`Unknown ticker: ${ticker}`));
} else {
resolve({ ticker, price: prices[ticker] });
}
}, Math.random() * 100);
});
}
fetchPrice("AAPL")
.then(data => {
console.log(`Fetched: ${data.ticker} at $${data.price}`);
return fetchPrice("MSFT");
})
.then(data => {
console.log(`Fetched: ${data.ticker} at $${data.price}`);
})
.catch(err => {
console.error("Fetch failed:", err.message);
});
Fetched: AAPL at $182.3
Fetched: MSFT at $415.1
new Promise((resolve, reject) => { ... }) wraps the asynchronous work. The .then() callback receives the resolved value; returning a new Promise from .then() means the next .then() in the chain waits for that Promise to settle. The chain is flat regardless of how many sequential operations it contains.
Chained .then() calls are sequentially readable — each step is at the same indentation level. The single .catch() at the end handles rejections from any step in the chain. Without Promises, each additional sequential step adds another level of nesting and requires its own error handler.
Promise.all for Parallel Execution
Fetching prices sequentially — wait for AAPL, then fetch MSFT, then fetch NVDA — is three times slower than fetching them all at once. Promise.all takes an array of promises, runs them concurrently, and resolves with an array of their results when all have resolved.
Replace the entire content of market.js with the following:
function fetchPrice(ticker) {
return new Promise((resolve, reject) => {
const delay = Math.floor(Math.random() * 100) + 20;
setTimeout(() => {
const prices = { AAPL: 182.30, MSFT: 415.10, NVDA: 875.50, TSLA: 172.80 };
if (!prices[ticker]) {
reject(new Error(`Unknown ticker: ${ticker}`));
} else {
resolve({ ticker, price: prices[ticker] });
}
}, delay);
});
}
const portfolio = {
AAPL: 100,
MSFT: 50,
NVDA: 30,
};
const tickers = Object.keys(portfolio);
const start = Date.now();
Promise.all(tickers.map(t => fetchPrice(t)))
.then(results => {
const elapsed = Date.now() - start;
console.log(`All ${results.length} prices fetched in ${elapsed}ms`);
let totalValue = 0;
results.forEach(({ ticker, price }) => {
const shares = portfolio[ticker];
const value = shares * price;
totalValue += value;
console.log(` ${ticker}: ${shares} shares @ $${price} = $${value.toLocaleString()}`);
});
console.log(`Total portfolio value: $${totalValue.toLocaleString()}`);
})
.catch(err => {
console.error("Portfolio fetch failed:", err.message);
});
All 3 prices fetched in 87ms
AAPL: 100 shares @ $182.3 = $18,230
MSFT: 50 shares @ $415.1 = $20,755
NVDA: 30 shares @ $875.5 = $26,265
Total portfolio value: $65,250
tickers.map(t => fetchPrice(t)) creates three promises immediately, all three requests begin in parallel, and Promise.all collects the results in the same order as the input array. The total wait time is determined by the slowest single fetch, not the sum of all fetches.
Three parallel fetches complete in roughly 87ms — the time of the slowest one. Three sequential fetches would take three times as long. Promise.all handles the coordination automatically; there is no counter, no race condition, and no edge case when the input array is empty (it resolves immediately with an empty array).
Note: If any promise in the array rejects, Promise.all rejects immediately with that error. The other fetches continue running, but their results are discarded. For cases where partial success is acceptable, use Promise.allSettled instead.
Promise.allSettled for Partial Failure
During market hours, some tickers are temporarily unavailable. The portfolio update should succeed for the four tickers that responded and flag the one that failed — not discard all five results because one API returned an error.
Replace the entire content of market.js with the following:
function fetchPrice(ticker) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const prices = { AAPL: 182.30, MSFT: 415.10, NVDA: 875.50, TSLA: 172.80 };
if (ticker === "GOOG") {
reject(new Error(`GOOG: API rate limit exceeded`));
} else if (!prices[ticker]) {
reject(new Error(`Unknown ticker: ${ticker}`));
} else {
resolve({ ticker, price: prices[ticker] });
}
}, Math.random() * 80 + 20);
});
}
const tickers = ["AAPL", "MSFT", "GOOG", "NVDA", "TSLA"];
Promise.allSettled(tickers.map(t => fetchPrice(t)))
.then(results => {
const succeeded = results.filter(r => r.status === "fulfilled");
const failed = results.filter(r => r.status === "rejected");
console.log(`Results: ${succeeded.length} succeeded, ${failed.length} failed\n`);
succeeded.forEach(r => {
const { ticker, price } = r.value;
console.log(` [OK] ${ticker}: $${price}`);
});
failed.forEach(r => {
console.log(` [FAIL] ${r.reason.message}`);
});
});
Results: 4 succeeded, 1 failed
[OK] AAPL: $182.3
[OK] MSFT: $415.1
[OK] NVDA: $875.5
[OK] TSLA: $172.8
[FAIL] GOOG: API rate limit exceeded
Promise.allSettled never rejects. Each result object in the array has a status of "fulfilled" (with a value property) or "rejected" (with a reason property). The four successful fetches are fully usable even though GOOG failed.
Promise.allSettled decouples the success path from the failure path. A single failed ticker does not abort the entire portfolio update. The calling code receives all results — successes and failures — and decides independently how to handle each. Using Promise.all here would cause the entire .then() handler to be skipped when GOOG fails.
async/await for Sequential Async Logic
async and await are syntactic sugar over promises. An async function returns a promise. await pauses the async function until the awaited promise settles, making sequential async logic read like synchronous code without nesting.
Replace the entire content of market.js with the following:
function fetchPrice(ticker) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const prices = { AAPL: 182.30, MSFT: 415.10, NVDA: 875.50, TSLA: 172.80 };
prices[ticker] ? resolve({ ticker, price: prices[ticker] })
: reject(new Error(`Unknown ticker: ${ticker}`));
}, Math.random() * 80 + 20);
});
}
function fetchExchangeRate(pair) {
return new Promise(resolve => {
setTimeout(() => {
const rates = { "USD/EUR": 0.92, "USD/GBP": 0.79 };
resolve(rates[pair] ?? 1.0);
}, 30);
});
}
async function getPositionInEur(ticker, shares) {
const { price } = await fetchPrice(ticker);
const rate = await fetchExchangeRate("USD/EUR");
const valueUsd = price * shares;
const valueEur = Math.round(valueUsd * rate * 100) / 100;
return { ticker, shares, priceUsd: price, valueEur };
}
async function buildPortfolioReport() {
try {
const positions = await Promise.all([
getPositionInEur("AAPL", 100),
getPositionInEur("MSFT", 50),
getPositionInEur("NVDA", 30),
]);
console.log("Portfolio (values in EUR):");
let total = 0;
positions.forEach(({ ticker, shares, priceUsd, valueEur }) => {
total += valueEur;
console.log(` ${ticker}: ${shares} shares @ $${priceUsd} = €${valueEur.toLocaleString()}`);
});
console.log(`Total: €${total.toLocaleString()}`);
} catch (err) {
console.error("Report failed:", err.message);
}
}
buildPortfolioReport();
Portfolio (values in EUR):
AAPL: 100 shares @ $182.3 = €16,771.6
MSFT: 50 shares @ $415.1 = €19,094.6
NVDA: 30 shares @ $875.5 = €24,163.8
Total: €60,030
await fetchPrice(ticker) pauses getPositionInEur until the price is available, then continues — reading like synchronous code but without blocking the event loop. try/catch handles rejected promises in async functions. The async function itself returns a Promise, so Promise.all can be applied to an array of getPositionInEur calls just as with any promise-returning function.
Sequential async steps — fetch price, then fetch exchange rate, then compute — are written as sequential lines, not nested .then() calls. The error handling is a single try/catch block rather than a .catch() at each step. The mental model matches the execution flow: top to bottom, waiting where necessary.
Promise.race for Timeouts
A slow price feed blocks the entire portfolio update if not bounded. Promise.race resolves or rejects with whichever promise settles first. A timeout implemented with Promise.race means no fetch can block indefinitely.
Add the following to market.js and run it:
function withTimeout(promise, ms, label) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error(`${label}: timed out after ${ms}ms`)), ms)
);
return Promise.race([promise, timeout]);
}
async function fetchWithTimeout(ticker) {
const fetch = new Promise(resolve => {
const delay = ticker === "SLOW" ? 500 : Math.random() * 80 + 20;
setTimeout(() => resolve({ ticker, price: 100.0 }), delay);
});
return withTimeout(fetch, 200, ticker);
}
async function main() {
const tickers = ["AAPL", "SLOW", "MSFT"];
const results = await Promise.allSettled(tickers.map(t => fetchWithTimeout(t)));
results.forEach(r => {
if (r.status === "fulfilled") {
console.log(`[OK] ${r.value.ticker}`);
} else {
console.log(`[FAIL] ${r.reason.message}`);
}
});
}
main();
[OK] AAPL
[FAIL] SLOW: timed out after 200ms
[OK] MSFT
withTimeout races the actual fetch against a timer. Whichever resolves or rejects first wins. The fetch promise is not cancelled — JavaScript has no built-in cancellation mechanism — but its result is ignored once the timeout wins. Combining withTimeout with Promise.allSettled means each ticker has an independent timeout and a single slow fetch cannot block the others.
Promise.race implements timeouts without timers embedded in business logic. withTimeout is a reusable wrapper that any promise can be passed through. Combining it with Promise.allSettled gives each fetch an independent deadline — the strategy the platform actually needs, expressed in four readable lines rather than interlocked callback state.
Summary
The market data aggregation service built in this tutorial demonstrates every major Promise capability in JavaScript:
new Promise((resolve, reject) => { ... })wraps asynchronous work; chained.then()calls are flat regardless of how many sequential steps the operation requires, and a single.catch()at the end handles rejections from any stepPromise.all(promises)runs all promises concurrently and resolves with all results in input order; total wait time equals the slowest single fetch, not the sum; rejects immediately if any promise rejectsPromise.allSettled(promises)always resolves with an array of{status, value/reason}objects; use it when partial success is acceptable and the caller needs to inspect both successes and failures independentlyasyncfunctions always return a Promise;awaitpauses execution inside the async function until the awaited promise settles;try/catchcatches rejections from awaited expressions just as it catches synchronous exceptionsPromise.race(promises)settles with whichever promise settles first; combine it with a timeout promise to bound the wait time for any single operation without changing the original fetch code