From 9e07efed0de26fc32e5be14a15f53dabbf3f7c14 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Mon, 15 Jan 2024 08:59:46 +0100 Subject: [PATCH] process: add deferTick Adds a new scheduling primitive to resolve zaldo when mixing traditional Node async programming with async/await and Promises. We cannot "fix" nextTick without breaking the whole ecosystem. nextTick usage should be discouraged and we should try to incrementally move to this new primitive. Refs: https://github.com/nodejs/node/issues/51156 Refs: https://github.com/nodejs/node/pull/51280 Refs: https://github.com/nodejs/node/pull/51114 Refs: https://github.com/nodejs/node/pull/51070 Refs: https://github.com/nodejs/undici/pull/2497 PR-URL: https://github.com/nodejs/node/pull/51471 --- doc/api/process.md | 68 ++++++++++++++++++++++++++ lib/internal/bootstrap/node.js | 3 +- lib/internal/process/task_queues.js | 76 +++++++++++++++-------------- test/async-hooks/test-defertick.js | 15 ++++++ 4 files changed, 125 insertions(+), 37 deletions(-) create mode 100644 test/async-hooks/test-defertick.js diff --git a/doc/api/process.md b/doc/api/process.md index b53986f04ae18c..af09227fc721ff 100644 --- a/doc/api/process.md +++ b/doc/api/process.md @@ -1219,6 +1219,74 @@ const process = require('node:process'); process.debugPort = 5858; ``` +## `process.deferTick(callback[, ...args])` + + + +* `callback` {Function} +* `...args` {any} Additional arguments to pass when invoking the `callback` + +`process.deferTick()` adds `callback` to the "defer tick queue". This queue is +fully drained after the current operation on the JavaScript stack runs to +completion and before the event loop is allowed to continue. It's possible to +create an infinite loop if one were to recursively call `process.deferTick()`. +See the [Event Loop][] guide for more background. + +Unlike `process.nextTick`, `process.deferTick()` will run after the "next tick +queue" and the microtask queue has been fully drained as to avoid Zalgo when +combinding traditional node asynchronous code with Promises. + +Consider the following example: + +```js +// uncaughtException! +setImmediate(async () => { + const e = await new Promise((resolve) => { + const e = new EventEmitter(); + resolve(e); + process.nextTick(() => { + e.emit('error', new Error('process.nextTick')); + }); + }); + e.on('error', () => {}); // e.emit executes before we reach this... +}); + +// uncaughtException! +setImmediate(async () => { + const e = await new Promise((resolve) => { + const e = new EventEmitter(); + resolve(e); + queueMicrotask(() => { + e.emit('error', new Error('queueMicrotask')); + }); + }); + e.on('error', () => {}); // e.emit executes before we reach this... +}); +``` + +In both of these cases the user will encounter an +`uncaughtException` error since the inner task +will execute before control is returned to the +caller of `await`. In order to fix this one should +use `process.deferTick` which will execute in the +expected order: + +```js +// OK! +setImmediate(async () => { + const e = await new Promise((resolve) => { + const e = new EventEmitter(); + resolve(e); + process.deferTick(() => { + e.emit('error', new Error('process.deferTick')); + }); + }); + e.on('error', () => {}); // e.emit executes *after* we reach this. +}); +``` + ## `process.disconnect()`