|
| 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