Skip to content

Commit be6f800

Browse files
committed
Add promise-queues.mdx
1 parent bc126e7 commit be6f800

2 files changed

Lines changed: 525 additions & 1 deletion

File tree

posts/promise-queues.mdx

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
---
2+
title: Promise queues
3+
tags: [TypeScript, JavaScript]
4+
description: A look at promise queues.
5+
slug: promise-queues
6+
date: 2026-01-05
7+
---
8+
9+
A promise queue is all about giving an order to functions that return promises:
10+
11+
```
12+
queue.add(() => fetch(<some-url>));
13+
```
14+
15+
<Note>
16+
It's **not** the return value of `fetch` (which is going to be a promise) that is queued, rather, it's a function that returns a promise.
17+
</Note>
18+
19+
We cannot do something like `forEach` or even `Promise.all`:
20+
<mark>They will not run one by one in order, that is, a task will not wait for the previous one to finish in order to start:</mark>
21+
22+
```
23+
tasks.forEach(task => task());
24+
```
25+
26+
For example, let's say we have a list of functions that return promises:
27+
28+
```ts
29+
function delay(ms: number, value: string) {
30+
return new Promise((resolve) => setTimeout(() => resolve(value), ms));
31+
}
32+
33+
const promiseProducers = [
34+
() => delay(1000, 'first'),
35+
() => delay(500, 'second'),
36+
() => delay(300, 'third'),
37+
];
38+
```
39+
40+
If we use something like this:
41+
42+
```ts
43+
promiseProducers.forEach(p => p().then(console.log));
44+
```
45+
46+
Of course, the third promise will resolve before the second one, and the second one will resolve before the first one.
47+
48+
#### Misconception of `Promise.all`
49+
50+
`Promise.all` is **not** a promise queue:
51+
52+
53+
```js
54+
Promise.all(tasks.map(task => task()));
55+
```
56+
57+
Again, a task will not wait for the previous one to finish in order to start.
58+
59+
### Brief refresher on the event loop
60+
61+
When it comes to the [JavaScript event loop](https://javascript.info/event-loop#event-loop), there are macrotasks (or, just tasks) and microtasks.
62+
63+
Microtasks are usually created with promises - the function inside `.then()`, for example, goes to the microtask queue.
64+
A microtask can also be created with the `queueMicrotask()` function.
65+
66+
The JavaScript engine prioritizes the microtask queue. It means that even after executing a macrotask, it does not wait for the macrotask queue to finish, instead immediately looks at the microtask queue.
67+
68+
So, something like this:
69+
70+
```js
71+
setTimeout(() => console.log("timeout 1")); // macrotask
72+
setTimeout(() => console.log("timeout 2")); // macrotask
73+
74+
Promise.resolve()
75+
.then(() => console.log("promise 1")); // microtask
76+
77+
Promise.resolve().then(() => { // microtask
78+
console.log('promise 2');
79+
setTimeout(() => console.log("timeout here")); // pushed to macrotask queue
80+
});
81+
82+
console.log('code'); // on the call stack
83+
```
84+
85+
Logs:
86+
87+
```
88+
code
89+
promise 1
90+
promise 2
91+
timeout 1
92+
timeout 2
93+
timeout here
94+
```
95+
96+
### Why do we want a promise queue?
97+
98+
We might want each promise generating function to wait for the previous one to be settled before it starts executing, or define a limit for the number of "concurrent" executions.
99+
100+
Reasons might be:
101+
- Preserving order for something like API calls that depend on each other
102+
- Processing files or database writes sequentially
103+
- Rate-limiting
104+
105+
---
106+
107+
A promise queue can be for executing promises either sequentially or concurrently.
108+
109+
> Note that JavaScript is [single-threaded](https://developer.mozilla.org/en-US/docs/Glossary/Thread) by nature, so at a given instant, only one task will be executing, although control can shift between different promises, making execution of the promises appear concurrent. [Parallel execution](https://en.wikipedia.org/wiki/Parallel_computing) in JavaScript can only be achieved through [worker threads](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API).
110+
111+
### Sequential execution
112+
113+
Something like this could work:
114+
115+
```ts
116+
type PromiseGen = () => Promise<any>;
117+
118+
class PQueue {
119+
queue: {
120+
task: PromiseGen;
121+
resolve: (value: any) => void;
122+
reject: (reason?: any) => void;
123+
}[];
124+
running: boolean;
125+
126+
constructor() {
127+
this.queue = [];
128+
this.running = false;
129+
}
130+
131+
add(task: PromiseGen) {
132+
return new Promise((resolve, reject) => {
133+
this.queue.push({ task, resolve, reject });
134+
this.runNext();
135+
});
136+
}
137+
138+
async runNext() {
139+
if (this.running || this.queue.length === 0) {
140+
return;
141+
}
142+
143+
this.running = true;
144+
const { task, resolve, reject } = this.queue.shift()!;
145+
146+
try {
147+
const result = await task();
148+
resolve(result);
149+
} catch (error) {
150+
reject(error);
151+
} finally {
152+
this.running = false;
153+
this.runNext();
154+
}
155+
}
156+
}
157+
158+
const queue = new PQueue();
159+
160+
function delay(ms: number, value: string) {
161+
return new Promise((resolve) => setTimeout(() => resolve(value), ms));
162+
}
163+
164+
queue.add(() => delay(1000, 'first')).then((value) => console.log(value));
165+
queue.add(() => delay(500, 'second')).then((value) => console.log(value));
166+
queue.add(() => delay(300, 'third')).then((value) => console.log(value));
167+
```
168+
169+
Logs:
170+
171+
```
172+
first
173+
second
174+
third
175+
```
176+
177+
No matter what the timeout value in the delay function is, the functions are run in the order they are added to the queue.
178+
179+
### "Concurrent" execution
180+
181+
The sequential queue we just wrote runs a maximum of one task at a time, and the next task starts only after the previous one finishes.
182+
183+
In the case of "concurrent" execution, we want to define a number for the maximum amount of tasks to run at a time, and when one task finishes, the next one in the queue will start running.
184+
185+
We can track the number of tasks that are currently running instead of tracking whether a task is running or not (like we do with `this.running`).
186+
187+
<Note>
188+
A new task will start only if the running count is less than the maximum amount given.
189+
</Note>
190+
191+
```ts
192+
type PromiseGen = () => Promise<any>;
193+
194+
class PQueue {
195+
maxConcurrent: number;
196+
queue: {
197+
task: PromiseGen;
198+
resolve: (value: any) => void;
199+
reject: (reason?: any) => void;
200+
}[];
201+
running: number;
202+
203+
constructor(maxConcurrent = 1) {
204+
this.maxConcurrent = maxConcurrent;
205+
this.queue = [];
206+
this.running = 0;
207+
}
208+
209+
add(task: () => Promise<any>) {
210+
return new Promise((resolve, reject) => {
211+
this.queue.push({ task, resolve, reject });
212+
this.runNext();
213+
});
214+
}
215+
216+
async runNext() {
217+
if (this.running >= this.maxConcurrent || this.queue.length === 0) {
218+
return;
219+
}
220+
221+
const { task, resolve, reject } = this.queue.shift()!;
222+
this.running++;
223+
224+
try {
225+
const result = await task();
226+
resolve(result);
227+
} catch (err) {
228+
reject(err);
229+
} finally {
230+
this.running--;
231+
this.runNext();
232+
}
233+
}
234+
}
235+
236+
const queue = new PQueue(2);
237+
238+
function delay(ms: number, value: string) {
239+
return new Promise((resolve) => setTimeout(() => resolve(value), ms));
240+
}
241+
242+
queue.add(() => delay(1000, 'first').then((value) => console.log(value)));
243+
queue.add(() => delay(500, 'second').then((value) => console.log(value)));
244+
queue.add(() => delay(300, 'third').then((value) => console.log(value)));
245+
queue.add(() => delay(400, 'fourth').then((value) => console.log(value)));
246+
```
247+
248+
Logs:
249+
250+
```
251+
second
252+
third
253+
first
254+
fourth
255+
```
256+
257+
Now, `running` is a number that holds the number of currently running tasks.
258+
We also have `maxConcurrent` to hold the maximum number of tasks to run "concurrently."
259+
260+
The running order looks like this:
261+
262+
- `() => delay(1000, 'first')` and `() => delay(500, 'second')` start immediately.
263+
- The second finishes and third starts.
264+
- The third finishes and fourth starts.
265+
- First finishes.
266+
- Fourth finishes.
267+
268+
One practical use of it might look like this:
269+
270+
```js
271+
const maxConcurrent = 4;
272+
const queue = new PQueue(maxConcurrent);
273+
274+
for (let file of droppedFiles) {
275+
let startUpload = () => {
276+
return fetch('/upload-file', {
277+
method: 'PUT',
278+
headers: { 'content-type': 'binary/octet-stream' },
279+
body: file
280+
});
281+
}
282+
283+
queue.add(startUpload);
284+
}
285+
```
286+
287+
More to read on promise queues:
288+
289+
- ["UI algorithms: a tiny promise queue"](https://blog.julik.nl/2025/05/a-tiny-promise-queue) by Julik Tarkhanov

0 commit comments

Comments
 (0)