Repository navigation
fix(js/ts): align tasks and cancellation with .NET - #5199
Open
leonitousconforti wants to merge 6 commits into
Open
leonitousconforti wants to merge 6 commits into
leonitousconforti wants to merge 6 commits into
Conversation
TaskCompletionSource differed from .NET in several ways: - SetCanceled works. The runtime spelt it SetCancelled, so every call raised "SetCanceled is not a function". SetCancelled is kept for JS code that calls it. - TrySetResult, TrySetException and TrySetCanceled return whether they completed the task. - A second SetResult, SetException or SetCanceled raises InvalidOperationException with the .NET message instead of being ignored. - SetException and TrySetException with a sequence fault the task with the first exception, which is what awaiting it throws on .NET. They rejected the task with the F# list itself. An empty sequence or a null element raises ArgumentException. - The non-generic TaskCompletionSource compiles to the same class instead of failing with "not supported by Fable".
Task.Run compiled to an import of a run function that the library did not export, so the module that used it failed to load. Task.Run now runs the function after the caller continues, for the Func<T>, Func<Task<T>>, Action and Func<Task> overloads, and unwraps a returned task. With a CancellationToken that is cancelled by the time it would run, the function does not run and the task is cancelled.
A faulted or cancelled task that nothing awaits is silent on .NET, but in JS its rejected promise was reported as an unhandled rejection, which ends a Node process by default. This happened for the tasks of TaskCompletionSource, Async.StartAsTask, Async.StartImmediateAsTask, Task.Run and a cancelled Task.Delay. They now get a rejection handler when they are created, as Async.StartChild children already do, and awaiters still see the failure.
CancellationToken.ThrowIfCancellationRequested raised a plain
Exception("Operation is cancelled"), and OperationCanceledException
constructed in user code compiled to a plain Exception, so
:? OperationCanceledException did not match either of them. Both now
create the library's OperationCanceledException, which the type test
already used. The constructor overloads that take a CancellationToken
drop it, the inner exception is kept, and the default message is .NET's
"The operation was canceled.".
- CancellationTokenSource.CreateLinkedTokenSource with one token, two tokens or an array of tokens was not supported by Fable. The linked source is cancelled when any parent is, at once if a parent is already cancelled, and cancelling it leaves the parents alone. Like .NET, a default token is skipped, no tokens raise ArgumentException, and disposing the source unlinks it from its parents. Dispose on a CancellationTokenSource compiled to nothing, so it now calls the source's Dispose, which does nothing for a source that is not linked. - CancellationTokenRegistration.Dispose and Unregister called directly were not supported by Fable. They remove the callback, and Unregister returns whether it was still registered. Like disposing through IDisposable, which already worked, Dispose does nothing for a default registration. CancellationTokenRegistration.Token is still not supported.
Async.AwaitTask of a cancelled task cancelled the awaiting workflow, so try/with did not catch it and the whole workflow was cancelled. Like .NET, it now raises TaskCanceledException through the error continuation, as fable-compiler#5064 did for Python. Async.AwaitPromise, Async.Parallel, Async.Sequential and Async.StartChild still treat an OperationCanceledException rejection as a cancellation. TaskCanceledException is a new OperationCanceledException subclass with .NET's "A task was canceled." message, and it can be constructed and type tested. The tasks of TaskCompletionSource.SetCanceled, Task.Run and Task.Delay with a cancelled token, and a cancelled Async.StartAsTask or Async.StartImmediateAsTask, reject with it, so awaiting them in task { } also raises TaskCanceledException as on .NET.
This was referenced Oct 28, 2026
This branch has not been deployed
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.
Tasks and cancellation in JS/TS now behave like .NET where they crashed, failed to compile or differed. One commit per concern, each with tests.
TaskCompletionSource:SetCanceledworks (the runtime spelt itSetCancelled, kept as an alias).TrySetResult/TrySetException/TrySetCanceledare supported, a secondSet*raisesInvalidOperationException, and the non-genericTaskCompletionSourcecompiles.SetException(seq<exn>)faulted the task with the F# list itself and now faults with the first exception.Task.Runcompiled to an import of a missingrun, so the whole module failed to load. Its overloads are supported, including an already-cancelled token.TaskCompletionSource,Async.StartAsTask,Task.RunorTask.Delay) no longer raises an unhandled promise rejection, as fix(js/ts): async cancellation propagation and continuation double-resolve #4746 did forStartChild. Awaiters still see the failure.ThrowIfCancellationRequestedraised a plainException, andOperationCanceledException()in user code compiled tonew Exception(), so:? OperationCanceledExceptionmatched neither. Both now create the library'sOperationCanceledException, with .NET's message.CancellationTokenSource.CreateLinkedTokenSourceand direct calls toCancellationTokenRegistration.Dispose()/Unregister()are supported (they didn't compile).Async.AwaitTaskof a cancelled task silently cancelled the awaiting workflow, skipping anytry/with. Like .NET, and like fix(python): settle cancelled tasks awaited by async workflows #5064 did for Python, it now raisesTaskCanceledException, a newOperationCanceledExceptionsubclass. Cancelled library tasks reject with it too, solet!intask { }sees .NET's type.Async.AwaitPromise,ParallelandStartChildare unchanged.Notes:
OperationCanceledException, soAwaitTaskraisesTaskCanceledExceptionfor both, where .NET raisesAggregateExceptionfor the second.Task.IsCompleted/Status,Task.Result, wrapping faults inAggregateException, unobserved failures oftask { }itself, andCancellationTokenRegistration.Token.