Fix forEach(async ...) causing fire-and-forget promises in notebook code#304898
Open
pefia wants to merge 2 commits intomicrosoft:mainfrom
Open
Fix forEach(async ...) causing fire-and-forget promises in notebook code#304898pefia wants to merge 2 commits intomicrosoft:mainfrom
pefia wants to merge 2 commits intomicrosoft:mainfrom
Conversation
Replace forEach(async ...) with for...of loops in two notebook locations where async callbacks were silently discarded, causing undo/redo operations and comment toggling to run concurrently instead of sequentially.
Contributor
There was a problem hiding this comment.
Pull request overview
Fixes async “fire-and-forget” behavior in notebook actions by replacing forEach(async ...) loops (which don’t await) with properly awaited for...of loops, ensuring operations complete (and errors propagate) before the command/undo-redo completes.
Changes:
- Make “Toggle Line Comment” across multiple selected cells await each cell’s async text model resolution and comment operation.
- Make notebook multicursor undo/redo await each undo/redo element in sequence instead of spawning untracked promises.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/vs/workbench/contrib/notebook/browser/controller/editActions.ts | Replaces selectedCells.forEach(async ...) with a for...of loop so the action’s promise resolves only after all cells are processed. |
| src/vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookMulticursor.ts | Replaces nested forEach(async ...) in multicursor undo/redo with awaited for...of loops to enforce correct completion and error propagation. |
src/vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookMulticursor.ts
Outdated
Show resolved
Hide resolved
Author
|
@microsoft-github-policy-service agree |
…notebookMulticursor.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What I Found
I was auditing the notebook codebase for async anti-patterns and I found two places where
Array.forEach(async ...)is used to drive async operations. This is a classic JavaScript footgun —forEachcompletely ignores the return value of its callback, so when you pass it anasyncfunction, the promises it creates just float away into the void. The calling code carries on as if everything is done, but nothing has actually happened yet.Bug 1: Notebook multicursor undo/redo runs all operations concurrently and resolves instantly
I found this in
notebookMulticursor.tsinsideupdateFinalUndoRedo(). Theundoandredohandlers that get pushed onto the undo/redo stack looked like this:There are actually two nested
forEach(async ...)calls here, which makes the problem even worse. What happens at runtime is:forEachfires off one un-awaited promise per resource in the mapundo()async function returns its promise right away — before a singleelement.undo()has completedI noticed that
value.reverse()is called in the undo path, which tells me the author intended for undo operations to run in reverse order. But withforEach(async ...), all the elements fire concurrently — thereverse()is meaningless. Redo has the same problem, just without the reverse.On top of all that, if any
element.undo()orelement.redo()throws, the error becomes an unhandled promise rejection — it never reaches the undo/redo service.What I did: I replaced both nested
forEach(async ...)withfor...ofloops. Now the outer loop iterates resources sequentially withfor (const value of newElementsMap.values()), and the inner loop awaits each element in order withfor (const element of value.reverse()). Theasyncfunction only resolves when all the work is truly done, and errors propagate correctly.Bug 2: Notebook "Toggle Line Comment" on multiple cells returns before doing anything
I found this in
editActions.tsin theCommentSelectedCellsActionclass. TherunWithContextmethod isasyncand returnsPromise<void>, but internally it does:So
runWithContextreturns immediately after kicking off the forEach — it doesn't wait for any cell to be processed. Each cell'sresolveTextModel()is async, so the actual comment toggling and selection restoration happen at some indeterminate time after the command "completes."In practice this probably works okay when there's only one cell selected (the async work happens quickly enough), but with multiple selected cells, they all race each other. If any cell's
resolveTextModel()is slow or fails, that's silently swallowed.What I did: I replaced
forEach(async ...)with afor...ofloop so each cell is processed sequentially: resolve the text model, toggle the comment, restore selections, then move to the next cell. TherunWithContextpromise now correctly represents when all the work is actually done.How I validated
npm run compile-check-ts-native— zero errors)forEach(async => ...)withfor...of await— so the logic inside each iteration is completely untouchedFiles changed
src/vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookMulticursor.ts— undo/redo handlerssrc/vs/workbench/contrib/notebook/browser/controller/editActions.ts— CommentSelectedCellsAction.runWithContext