Microtask queue execution after Isolate::TerminateExecution() timeout
0 reputation · 30 Dec 2021, 02:20 UTC
0 reputation · 30 Dec 2021, 02:20 UTC
When an embedder enforces a script timeout by calling Isolate::TerminateExecution(), V8 sets an internal flag that causes the next interrupt check to throw a special Terminated exception. The embedder catches this exception via TryCatch and must call CancelTerminateExecution() to reset the isolate for reuse.
The documented behavior states that termination is "best effort" and does not guarantee immediate stop. A specific unresolved aspect is microtask queue handling: if termination occurs while a promise reaction is running, already-queued microtasks may continue to execute before the Terminated exception propagates. This can produce observable side effects—such as promise resolutions, global mutations, or additional async scheduling—after the logical timeout deadline has passed.
The V8 source indicates interrupt checks occur at specific points in generated code and certain runtime calls, but the exact placement varies by version and optimization tier. WebAssembly code without explicit interrupt checks and long-running native operations (regex, JSON.parse) can further delay termination delivery.
Terminated exception unwinds the stack, or only microtasks already queued at the moment of the interrupt check?CancelTerminateExecution() when the embedder attempts to reuse the isolate?35775 reputation · 30 Dec 2021, 06:52 UTC
Do not assume that the microtask queue drains completely before a termination exception unwinds. Equally, do not assume that every queued-but-unstarted microtask is suppressed. Isolate::TerminateExecution() requests best-effort termination; the observable result depends on where V8 reaches an interrupt check.
There is no supported public operation that purges pending microtasks. CancelTerminateExecution() clears the termination request, but it does not clear the microtask queue, cancel promise callbacks, or undo side effects already performed.
| Point | What embedders can rely on |
|---|---|
| Termination | Termination is requested and delivered when execution reaches an interrupt check. The deadline is when termination was requested, not necessarily when JavaScript observes it. |
| Cancellation | CancelTerminateExecution() clears the request only. |
| Pending microtasks | The public microtask API provides scheduling and checkpoint control, but no supported purge operation. |
| Side effects | Completed promise reactions, global mutations, and native operations are not rolled back. |
The likely explanation for post-timeout callbacks is that an interrupt check inside a running promise reaction unwinds that reaction and interrupts its checkpoint. Other queued microtasks may remain pending. Whether they subsequently run is version- and context-sensitive, so neither a complete drain nor complete suppression should be treated as a stable contract.
MicrotasksPolicy::kExplicit when the embedder must control checkpoint boundaries. This prevents automatic checkpoints from obscuring where stale work resumes; it does not remove queued work.TryCatch::HasTerminated(), then call CancelTerminateExecution(). Do not cancel before the termination has been observed.Use a minimal embedder test with counters in several promise callbacks. Terminate while the first callback runs, record which counters advance, whether HasTerminated() is true, and whether a checkpoint-completion notification fires before or after cancellation. Repeat with termination requested before a checkpoint, and test every supported V8 version in debug and release builds. Record public behavior rather than internal interrupt locations.
The one diagnostic detail that changes the recommendation is whether the same isolate must be reused with a strict guarantee of no post-deadline side effects. If yes, isolate disposal is the defensible boundary.
Use comments to ask for clarification. Post a solution as an answer.
35,775 reputation · 30 Dec 2021, 08:46 UTC
The key detail is that V8 delivers the Terminated exception at the next interrupt or microtask checkpoint after the flag is observed, not at a fixed queue boundary. If a running promise reaction enqueues additional microtasks and a checkpoint is reached before the exception unwinds, those newly queued reactions will execute—producing observable side effects past the nominal deadline. Conversely, microtasks already in the queue but not yet reached by a checkpoint may not run before the exception propagates.
Because CancelTerminateExecution() only clears the terminate flag, any microtasks that remain queued (or are added by a finally block in the embedder's catch path) can execute on the next entry into the isolate. Treat the isolate as potentially contaminated after a timeout; the safest pattern for reuse is to discard the context or isolate entirely rather than relying on queue drainage.