How to Resolve Promises Sequentially in JavaScript — Jamdesk Blog<br>For AI agents: the site index is at /llms.txt and the full site content at /llms-full.txt. For a markdown version of a page, append .md to its URL or request it with Accept: text/markdown. llms.txt<br>In JavaScript, the Promise.all() function is one of your most powerful tools in your coding toolkit, especially when you have async work. You can fire everything off, wait for the slowest one to complete, and then continue processing. However, sometimes running it all at once is exactly what breaks production.<br>When you need promises to run in order, one finishing before the next begins, the answer is a for...of loop with await inside it. Easy right? Well, the catch is a subtlety that bites almost everyone the first time; if you've already created your promises, they've already started (whoops). Awaiting them in order doesn't make the work sequential. It just makes you wait while it all runs at once anyway.<br>Why Would You Run Promises Sequentially?<br>Because all-at-once isn't free. Every concurrent promise is a concurrent connection, a concurrent write, another request landing on something that may have a rate limit.<br>On the social API I worked on at my previous company, we had a sync job that pulled a few thousand user records by calling admin.auth().getUser() for each UID. The first version mapped them straight into Promise.all(). Beautiful in dev with ten test accounts. In production it slammed thousands of simultaneous requests into Firebase Auth, tripped the rate limit, and the whole job fell over. Firebase caps these admin lookups precisely so one client can't do what we'd just done (Firebase Admin SDK docs).<br>The solution wasn't more retries, but rather to stop asking for everything at once. Sequential resolution gave the downstream service room to breathe, and the job started finishing instead of failing.<br>Same story shows up whenever order matters: database migrations that have to run in sequence, writes where step two depends on step one landing first, or any API that starts returning 429 the second you get enthusiastic.<br>Why forEach and map Won't Wait<br>This is the crux of the problem that sends most people searching for an answer in the first place: you use forEach, add an await inside each callback fully expecting it to pause between items, and instead the loop sprints right past every one of them without waiting. Unfortunately, this doesn't work, and yes, JavaScript can sometime suck.<br>// Looks sequential. Isn't.<br>[id1, id2, id3].forEach(async (id) => {<br>const user = await getUser(id);<br>console.log(user.name);<br>});<br>console.log("Done!"); // Prints FIRST, before any user
forEach doesn't understand promises. The callback returns one, forEach shrugs and throws it away, and all three lookups launch on the same tick. "Done!" logs before a single user comes back, and you haven't sequenced anything. You've just made the chaos harder to see.<br>The same applies to .map() if you're after ordering. .map() is great for building an array of promises to hand to Promise.all(), but the mapping itself kicks every promise off immediately. Map for concurrency. Loop for sequence.<br>The Sequential Pattern: for...of With await<br>This is the version that actually works. A plain for...of loop pauses at each await, so the next iteration doesn't start until the current promise resolves.<br>const resolveSequentially = async (tasks) => {<br>const results = [];<br>for (const task of tasks) {<br>results.push(await task());<br>return results;<br>};
Notice task(), not task. That parenthesis is the entire point. We pass an array of functions , and call each one inside the loop, so the promise isn't created until its turn comes up. Defer the creation and you defer the work.<br>Compare the two ways to feed it:<br>// Wrong: every getUser() fires during .map(), all at once. Awaiting them<br>// in a loop afterward orders the results, not the work.<br>const started = uids.map((uid) => getUser(uid));<br>const results = [];<br>for (const p of started) results.push(await p);
// Right: each getUser() fires only when the loop reaches it.<br>const tasks = uids.map((uid) => () => getUser(uid));<br>await resolveSequentially(tasks);
The difference is all timing: the first version calls getUser while building the array, so every request is already in flight before the loop even runs. Only the second one actually protects Firebase, because only the second one defers each call until the loop reaches it. This is the bit most tutorials gloss over. If your goal is to ease load and not just collect results in order, you have to hand the loop work it hasn't begun yet.<br>How Do You Handle Errors Mid-Loop?<br>One rejection in a for...of loop throws straight out and abandons every task you hadn't reached yet. Sometimes that's what you want. Often it isn't, especially in a batch job where one bad record shouldn't sink the other 2,000.<br>Wrap each call and decide per item:<br>const resolveSettled = async (tasks) => {<br>const results = [];<br>for (const task of tasks)...