Skip to content

TaskEx: ignore #140

Description

@bartelink

Replaces #128. TaskEx top level issue: #139

Async.Ignore has always been ugly and undiscoverable. While I tend to make ignoring explicit by using let! _ = <async stuff I want to ignore result of>, it's commonly the last expression in a function, and having to do let! _ = <thing I'm wrapping> in () is too much.

It is proposed that the module associated with any given builder should by convention have an ignore function that correctly observes completion of the work, dropping the result, but propagating exceptions, if any

Current proposed APIs that are not currently in FSharp.Core (will be updated inline based on discussion below):

module Async =
    let inline ignore (a : Async<'t>) = Async.Ignore
module Task =
    let inline ignore (t : Task<'t>) = ...
module ValueTask =
    let inline ignore (t : ValueTask<'t>) = ...

NOTES:

  1. Currently, TaskSeq (and other libs such as IcedTasks contain various bespoke implementations
  2. While this could live in a TaskEx lib, it could be argued that FSharp.Core is the obvious home for them. (However that would raise the issue of whether they need to go into the 6.x release line in order to align with minimal dependencies for various common libraries)

Activity

  1. changed the title [-]TaskShims: ignore[/-] [+]TaskEx: ignore[/+] on Jan 10, 2023
  2. added
    feature requestNew feature or enhancement request
    topic: task-exRelated to the proposed new TaskEx library, which should get its own oss haven
    on Oct 29, 2023
  3. added this to the vFuture milestone on Mar 18, 2024
  4. github-actions commented on Mar 18, 2026

    @github-actions
  5. bartelink commented on Mar 18, 2026

    @bartelink
    MemberAuthor

    Good news — the ignore functions this issue proposes are already implemented in the library! They live in Utils.fs and are exported via Utils.fsi

    Image

    I guess these nudges are slowly shuffling me toward getting AwaitTaskCorrect into FSharp.Core

    @T-Gro (or anyone else) Do you have a stance on getting these sorts of helpers into core? (ideally the overall picture of #139 is taken into account in any answer)

  6. T-Gro commented on Mar 18, 2026

    @T-Gro

    The suggestion is approved in principle, fsharp/fslang-suggestions#840 (comment) .

    I do not like the name and usage, but I do agree with what it does internally.
    Let me refresh the discussion and wait for proposals.

  7. bartelink commented on Mar 18, 2026

    @bartelink
    MemberAuthor

    @T-Gro thanks for the response

    I'm well aware of the AwaitTaskCorrect suggestion and it's state - it's my pet hate but it's not like I couldn't fix it; I just haven't gotten around to it

    I was asking about your/a stance on things like providing a Task.ignore and ancillary functions as mapped out in #139.

    Summarizing the relevant points:

    • this lib and many others have these sorts of helpers
    • apps should not really be having to take dependencies on libs like this (esp if they don't use IAsyncEnumerable) to cover basic needs (or doing stuff like let! _ = taskThing in () that should really be something like do! taskThing |> Task.ignore).
    • Obviously we need to be sane about the amount of one-liners admitted into FSharp.Core

    For some the answer is FsToolkit and/or IcedTasks.

    My workaround hack is (as in the case in this library) to sprinkle them around in Internal modules like this: https://github.com/jet/propulsion/blob/master/src/Propulsion/Internal.fs#L160

    The bottom line is that the helpers that live in FSharp.Control.TaskSeq nuget are pretty close to what I'd want/recommend, but they dilute the focus of this library, and don't solve the problem that we have N copies of these throughout the ecosystem that don't provide any useful variation - they just mean newcomers spend ages working out where all the little incidental helpers come from and/or why they exist.

    Do you see a potential for any of these helpers to go live in FSharp.Core ? Does having a FSharp.TaskEx make more sense from your perspective (I'm not convinced personally as such a thing has huge potential to become a junk drawer unless it has a very strong maintainer group, along with general diamond dependency structure concerns)?

    The current state of play is bad news for day to day real world F# consumption in writing/maintaining apps unless you:

    • pull in Equinox+Propulsion, TaskSeq and/or FsToolkit (FSharpPlus and many others probably provide similar - these are just ones I've had lots of day to day experience with)
    • are an experienced/senior dev that's bootstrapped on lots of nuances of Async/Task interop and tradeoffs that can reliably work out the one liner shims appropriate/necessary in the context of a given application suite.
  8. T-Gro commented on Mar 18, 2026

    @T-Gro

    Let me collect some data on the redefinitions and usage (under multiple names, possibly), to have a better understanding of what is needed by most.

    Those can live in FSharp.Core, we should just better understand the real usage for each (i.e. not hide less significant API additons under the same umbrella).

    I will transitively process all issues linked from your comment and see what I find at least in public GH repos.

  9. T-Gro commented on Mar 18, 2026

    @T-Gro

    Proposed F# Task/Async API Additions — Verified GitHub Usage

    Date: 2026-03-18
    Sources: TaskSeq#139, #140, #141, #142, #143, fslang-suggestions#840, jet/propulsion Internal.fs, IcedTasks AsyncEx.fs, FsToolkit Async.fs+Task.fs
    Methodology: 7 subagents opened actual source code on GitHub, verified declarations vs usages, deduplicated forks, classified each hit

    Results

    # Proposed API Signature Hits Decl Repos Use Repos TaskSeq IcedTasks FsToolkit F#+ Source
    1 Async.map ('T→'U) → Async<'T> → Async<'U> 720 25 25 ✅ uses ✅ ✅ Utils
    2 Async.parallelLimit/Throttled int → Async<'T> seq → Async<'T[]> 113 17 26 ❌ ❌ inline ❌ #143
    3 Async.bind ('T→Async<'U>) → Async<'T> → Async<'U> 432 13 24 ✅ ❌ ✅ ❌ Utils
    4 Task.ignore Task<'T> → Task / Task<unit> 140 8 22 ✅ ❌ ✅ ✅† #140
    5 Task.map ('T→'U) → Task<'T> → Task<'U> 330 11 17 ✅ ❌ ✅ ✅ Utils
    6 Task.fromResult/singleton 'T → Task<'T> 148 7 21 ✅ ❌ ✅ ❌ Utils
    7 Async.ofTask (AwaitTaskCorrect) Task<'T> → Async<'T> 90 10 14 ✅‡ ✅ ❌ ✅ #141/#840
    8 Task.bind ('T→Task<'U>) → Task<'T> → Task<'U> 83 10 11 ✅ ❌ ✅ ✅ Utils
    9 Async.singleton 'T → Async<'T> 88 5 16 ❌ ❌ ✅ ❌ FsToolkit
    10 Task.ofAsync/Async.toTask Async<'T> → Task<'T> 11 8 2 ✅ ❌ ❌ ❌ Utils
    11 Task.ofUnitTask/ofUnit Task → Task<unit> 15 4 5 ✅ ❌ ✅ ❌ Utils
    12 Task.catch Task<'T> → Task<Choice<'T,exn>> 14 3 5 ❌ ❌ ✅ ❌ FsToolkit
    13 ValueTask.ignore ValueTask<'T> → ValueTask 6 2 3 ✅ ❌ ❌ ✅† #140
    14 Task.toAsync Task<'T> → Async<'T> 6 2 2 ✅ ❌ ❌ ❌ #141
    15 Async.call (CT→Task<'T>) → Async<'T> 10 2 2 ❌ ❌ ❌ ❌ Propulsion
    16 Async.ofUnitTask Task → Async<unit> 4 1 2 ✅ ✅ ❌ ❌ #141
    17 Async.startImmediateAsTask CT → Async<'T> → Task<'T> 17 2 1 ❌ ❌ ❌ ❌ #142
    18 Async.ignore (lowercase) Async<'T> → Async<unit> 0 0 0 ✅ ❌ ❌ ❌ #140

    Legend

    Symbol Meaning
    Hits GitHub code search file matches across ALL of GitHub (forks excluded)
    Decl Repos Unique non-fork repos that define the helper (verified by reading source)
    Use Repos Unique non-fork repos that call it (consuming, not defining)
    ✅‡ TaskSeq wraps Async.AwaitTask without AggregateException fix
    ✅† Present but different semantics (F#+: Task.ignore = Task→Task<unit>; ValueTask.ignore = ValueTask→ValueTask<unit>)
    inline Uses Async.Parallel(…, maxDegreeOfParallelism=N) inline, no named helper
    uses IcedTasks calls Async.map internally but does not declare it

    Library Coverage Summary

    Library Coverage Notes
    TaskSeq 15/18 Most comprehensive. Async.ofTask wraps Async.AwaitTask without AggregateException fix. Missing: parallelLimit, Async.singleton, Task.catch
    FsToolkit 9/18 Dominant NuGet source for downstream usage. Missing: AwaitTaskCorrect, conversions, parallelLimit
    FSharpPlus 6/18 Has AwaitTaskCorrect (as Async.Await). Task.ignore/ValueTask.ignore have inverted signatures
    IcedTasks 2/18 Focused on CEs (asyncEx, cancellableTask), not utility module functions

    Notable Findings

    • Async.map (25 declaring repos) — the F# compiler itself (dotnet/fsharp) declares and uses this in lib.fs. The single most duplicated helper in the ecosystem.
    • Async.parallelLimit (17 declaring repos) — two implementation patterns observed: simple Async.Parallel wrapper vs SemaphoreSlim-based throttling. Names vary: parallelLimit, parallelThrottled, ParallelWithThrottle.
    • AwaitTaskCorrect (10 declaring repos) — approved-in-principle since 2022 but unimplemented. The canonical fssnip is still copy-pasted. 6 additional repos wrap Async.AwaitTask under the Async.ofTask name without the AggregateException fix.
    • Task.ignore — two semantic variants in the wild: Task<'T> → Task<unit> (FsToolkit, most common) vs Task<'T> → Task (simple upcast, less common).
    • Async.ignore (lowercase) has zero independent implementations outside TaskSeq. Nobody wraps Async.Ignore in lowercase. Drop from proposal.
    • Async.startImmediateAsTask pipeable wrapper — only 2 repos: jet/propulsion and ionide/FsAutoComplete.
    • No single library covers all 18 APIs. TaskSeq comes closest (15/18) but its Async.ofTask lacks the AggregateException fix.

    Top Declaring Repos

    Repo APIs declared
    fsprojects/FSharp.Control.TaskSeq Task.ignore, Task.map, Task.bind, Task.fromResult, Task.ofAsync, Task.toAsync, Task.ofUnitTask, ValueTask.ignore, Async.ofTask‡, Async.ofUnitTask, Async.toTask, Async.ignore, Async.map, Async.bind
    jet/propulsion Task.ignore, Async.ofTask✅, Async.ofUnitTask, Async.call, Async.startImmediateAsTask, Async.executeAsTask, Async.parallelLimit, Task.parallelLimit, Task.catch, Task.ofUnitTask
    demystifyfp/FsToolkit.ErrorHandling Task.map, Task.bind, Task.ignore, Task.singleton, Task.ofUnit, Task.catch, Async.map, Async.bind, Async.singleton
    fsprojects/FSharpPlus Task.map, Task.bind, Task.ignore†, Async.map, Async.ofTask (as Async.Await), ValueTask.ignore†
  10. bartelink commented on Mar 18, 2026

    @bartelink
    MemberAuthor

    Hm; general thoughts re the above:

    • (unsurprisingly) plenty hallucinations in the summary, but (also not unsurprisingly) it's not useless
    • I'd exclude .map. Just because it's common doesn't make it a good thing to have in the box IMO
    • Task.ofAsync and some related ones are questionable as it makes it too easy not to flow cancellation etc
    • the ofUnit* specialization to cover Task vs Task<'T> feels icky so should be deferred/handled separately
    • Async.ignore (with that casing) is not something to pooh-pooh - the fact that prod code uses Async.Ignore, Async.Catch, Async.StartAsTask/StartImmediateAsTask (with and without threading cancellation, with or without actually meaning StartImmediateAsTask but using StartAsTask as it sounded like the right thing) was understandable 10 years ago but the myriad inconsistencies and pitfalls are why all these helpers have spawned and/or continue to proliferate in various ways with useless variation clouding things.

    Before this can be converted into something for broader discussion, some high level stances would help:

    • a stance on whether we err on the side of keeping warts in the interest of avoiding confusing duplication (e.g. add a Task.ignore but trust people will discover/understand that Async.Ignore is cased that way for reasons and that's just the way it is)
    • a stance on whether we put things with pitfalls into the mix or not (Task.ofAsync without some way to surface that you probably want to flow CT in most cases)
    • a stance on long excluded things like Async.map (I think that's one to keep off the table; its definitely highly improbable that it's not a deliberate omission that's had eons of airtime)

    The elephant in the room re all of the above is that fleshing out an Async.Await that does not egregiously introduce AggregateExceptions probably will answer a lot of the above indirectly as a side-effect of getting it done.

  11. T-Gro commented on Mar 18, 2026

    @T-Gro
    1. Avoid confusing duplication. If current state is really bad, I would rather do Obsolete + better thing, but has to be justified. Mere casing does not meet the bar I am afraid.
    2. Correctness by default, make pitfalls explicit (e.g. via CancellationToken.None). Humans as well as agents will try to take shortcuts for sure, but at least it makes them visible.
    3. If something is difficult to get right/has many tradeoffs, but people still reimplement their own versions - we are not winning anything by excluding it. If it is needed and used a lot more then the others, we should ship a sensible default for it IMO.
  12. bartelink commented on Mar 18, 2026

    @bartelink
    MemberAuthor

    Thanks.

    1. Fair enough NOTE1
    2. Great. For avoidance of doubt, that rules out Task.ofAsync
    3. Sounds like a taking an Async.map + Task.map proposal around the houses could flesh out a lot of the pros/cons, leaving filling out the rest of the matrix a more constrained ask. NOTE2

    NOTE1 but I HATE the Async.Ignore wart; thing is I can add an inline one-liner in my personal helpers. Its very easy for these warts to drive people to the conclusion that the answer is to just use Task for all the things and treat Async as some unfortunate historical path. i.e. if I can do Task.ignore and ValueTask.ignore but Async is not in alignment.)

    NOTE2 But: If it's in the box, it becomes blessed. It will inevitably lead to clear 3 line async or task blocks becoming more terse piped equivalents, for better or for worse (it's clearly subjective)

    I personally think we're better off without it, but definitely not a hill I want to die in, especially if having some agreement means we have consistent Task/Async/ValueTask helper names going forward, and a pattern for FsToolkit, IcedTasks and/or other computation expressions to follow.

  13. T-Gro commented on Mar 18, 2026

    @T-Gro

    RE NOTE1: But wouldn't you then hate also other PascalCase'd members eventually, in places where a Module.function is expected by you?
    1 more option in our toolbox I forgot about - keep the "worse" variant, do not mark it obsolete (so prevent warnings), but do [<EditorBrowsable(Never)>] so that it isn't offered for newly written code.

  14. bartelink commented on Mar 18, 2026

    @bartelink
    MemberAuthor

    1 more option in our toolbox I forgot about - keep the "worse" variant, do not mark it obsolete (so prevent warnings), but do [<EditorBrowsable(Never)>] so that it isn't offered for newly written code

    Good point - I think that might be a potential answer for Async.Await supplanting Async.AwaitTask.

    Whether it has a role in dealing with Async.Ignore vs Task.ignore vs Task.Ignore is a longer discussion...

    RE NOTE1: But wouldn't you then hate also other PascalCase'd members eventually, in places where a Module.function is expected by you?

    AIUI The pattern in this lib and in general is to have camelCase names (sometimes actually implemented as members to facilitate overloading. Task.ignore clearly follows that trend. If we were to be consistent with how Async lays it out, that should clearly be named Task.Ignore.

    I was already referring to there being more instances by calling out Async.Catch not aligning with a proposed Task.catch.

    My main concern is with 'othering' Async (even though the ecosystem gaining consistent and predictable Task, TaskSeq, ValueTask functions across the board is a big win). So yes, in my world one would add Async.ignore, .catch and more, and conceal the PascalCase ones from auto completion (especially if Task, TaskSeq and N other things only have them as camelCase named functions). There's plenty code out there with direct consumption of Async.Ignore and I can appreciate that the original naming and alignment with the FDG signatures and policies are valid for many obvious reasons.

    I guess this does raise the point that there are multiple concerns at play in the Async case that don't matter as much for the others:

    • consistent FDG compliant API suitable for use from C# and other .NET languages
    • large installed base that can't and/or shouldn't be forced to switch

    I guess if Async remains othered for the moment, potentially shims can live in an AsyncEx providing an Async.* API surface that makes it align with ValueTask.* vs Task.*. Or you can flip that and say that Task.Ignore is the correct name/API design for consistency.


    All of which brings us back to the start of the circle:

    • do low value and/or arguably duplicative shims and sugar really belong in FSharp.Core, or would a dedicated lib in fsproject make more sense?
    • who are the realistic maintainers of such an API set? if we can answer that, we'll know pretty quick whether a Task.map and an Async.map are one of the first functions that get implemented or not!

    My initial assumption, loosely held, was that doing Await, then Ignore, then StartImmediateAsTask(forcing CT to be supplied) and incrementally was the best approach.

    Your calling out of my wanting an Async.ignore for reasons is correct. It seems that high level direction/planning as to whether one liners and duplication can be all but ruled in or out on some basis needs to be the first step though - doesn't feel like something that one can do by voting and/or mapping out what the current patterns happen to be - there need to be high level principles (and to be fair there's significant clarity already, but duplicating functions, having map or not, are two examples where people are probably seeing/hearing what they want to at present!).

  15. 20 remaining items

  16. bartelink commented on May 12, 2026

    @bartelink
    MemberAuthor

    cc @gusty I'm thinking you've been around the houses on many of these things

    If you have the time would appreciate some comments here re fsharp/fslang-suggestions#1466 and fsharp/fslang-suggestions#1467 - especially if there's stuff you really don't like, consider superfluous, or a minor tweak would improve it. Ditto naming improvements before we spread it wider (maybe via @sergey-tihon ?) would be more than welcome.

    Is there anyone else that should be CC'd on this for a first round of getting rid of obvious cruft or extremely commonly helpful/needed small functions? @eiriktsarpalis

  17. T-Gro commented on May 13, 2026

    @T-Gro

    I removed that comment 👍 , thanks for fixing it.

    Once you are ready with final touches on those two suggestions, we might invite people via Discord - this is for sure the most active F# social and people will come with opinions :).

    re: startAsync vs runAsync -
    I would pick startAsync as well (it's taking a computation and starting it into a hot task)

  18. bartelink commented on May 13, 2026

    @bartelink
    MemberAuthor

    @T-Gro Thanks

    @T-Gro I don't have any further things I want to change in either suggestion at this point, but just want to finalize re startAsync/runAsync as for me your comments are inconclusive re the following:

    type Async with
         // https://github.com/fsharp/fslang-suggestions/issues/1042
         // NOTE name is so it appears in completion list near Async.RunSynchronously
         // Still open to suggestions to intuitively convey that this is RunSynchronouslyCorrect though - see #1042
         member _.RunSynchronouslyImmediate(computation: Async<'T>): unit =
             // runs on this thread, without implicit hop to thread pool, so has a correct stack trace
         // NOTE name has Task in it to a) allude to Task.Run b) align with [Value]Task.runAsync
         member _.RunTask(createTask: CancellationToken -> Task): Async<unit> = ...
         ... and further overloads of RunTask ...
    module Task =
         // NOTE name aligns with Async.runTask, forces flowing of cancellation
         let runAsync (computation: Async<'T>): CancellationToken -> Task<'T> = // Async.StartImmediateAsTask...
    
         // TOCONSIDER more natural for use with piping - as per runAsync but with the arguments flipped
    
         //  member _.Action1(arg, ct): Task<'T> =
         //       async { do! x arg; return y }
         //       |> Task.startAsync ct
         //  member _.Action2(arg, ct): Task<'T> = Task.startAsync ct <| async {
         //       do! x arg
         //       return y }
         //  member _.Action3(arg, ct): Task<'T> =
         //      Task.startAsync ct <| async {
         //           do! x arg
         //           return y }
         //let startAsync (ct: CancellationToken) (computation: Async<'T>): Task<'T> = runAsync computation ct

    IME it's very clear from lots of usage that startAsync is the more useful signature (in terms of arg order, even if the CT is logically the last part that should be being supplied from a partial application perspective).

    The questions to be decided are:

    • is the fact the arg order is slightly illogical a concern (probably moot if nobody is going to use a technically more logical one)
    • should both be present (Task.runAsync is the logical inverse of Async.runTask)
    • if only one is present, which one, and what name
    • assuming its one function and the name is based off start, should it be Task.start, Task.startImmediate, Task.startAsyncImmediate or something else given there's an Async.RunSynchronouslyImmediate being added at the same time ?

    It'd be great to have your thoughts on this so the first cut isnt leaving lots of stuff open (its not a problem if people come with other opinions; it's more about having something concrete so any improvement/alteration suggestions can be equally concrete)

  19. TheAngryByrd commented on May 13, 2026

    @TheAngryByrd
    Collaborator

    IME unless you have IcedTasks/CancellableTask/async2 in your stack, async can still make sense for quite a few layers as you get cancellation token propagation without it being in your face, but ASP.NET and/or a C# top layer can speak Tasks and explicit cancellation token wiring?

    What's in the box is important or people without the full array of tools mapped out in their heads will say "hm, well in C# and ASP.NET I pass a CancellationToken everywhere and end up just using bald task { and/or punting on correct cancellation token propagation like the average C# impl?

    I'm not arguing it wouldn't be useful, there are plenty of times you have an outside CancellationToken and an Async<_> and you want to fuse the two. ASP.NET is just the time that shows up regularly with this scenario. Other scenarios like in fsac also have this problem because a cancellation can come from an LSP client.

    I'm just saying that I don't have this scenario often anymore as I tend to use IcedTasks when possible.

  20. gusty commented on May 13, 2026

    @gusty
    Member

    Thanks @bartelink I added my thoughts to the discussion.

    Regarding the replacements of Async.StartImmediateAsTask isn't it worth a separate issue for that (and revisiting the default cancellation token stuff), as you suggested long time ago?

  21. bartelink commented on May 13, 2026

    @bartelink
    MemberAuthor

    Other scenarios like in fsac also have this problem because a cancellation can come from an LSP client.

    @TheAngryByrd For the needs of the fsac usage above, assuming what I think T-Gro is asking for, AIUI, this could be written as:

    UnusedOpens.getUnusedOpens (tyRes.GetCheckResults, getSourceLine)
    |> Task.startAsync progress.CancellationToken
    |> Async.Await // assuming asyncEx does not Bind to `Task<'T>`

    Of course I may be entirely missing the point ;)

    I'm really just trying to bottom out on whether the Async.withCancellation suggestion can be covered or 90% covered by some combination of smaller helpers - i.e. if there is a real need an impl involving TaskCompletionSource gymnastics and it's remotely common, putting it in the box is on the table. But if we're providing a Task.startAsync that has a non-optional CancellationToken and the only awkwardness is whether an Async.Await is required based on the specific bindings available at the call site then I'd be seeking to close that suggestion with a workaround of "this can be done succinctly via Task.startAsync ct |> Async.Await"

  22. bartelink commented on May 13, 2026

    @bartelink
    MemberAuthor

    Hi @gusty and thanks for responding.

    Thanks so much for the detailed response on 1466 - will respond over there regarding those.

    I'm a bit more confused re your other points so apologies in advance for the wall of text below ;)

    Regarding the replacements of Async.StartImmediateAsTask isn't it worth a separate issue for that (and revisiting the default cancellation token stuff)

    I was aware of the discussion you linked (I've traversed all these tickets including all discussions in depth in recent times). In that case I was suggesting that there should be a separate tracking item wrt stuff that brings the Async.DefaultCancellationToken into play. That's various type Async members that have a ?cancellationToken argument. The suggestions here are specifically about functions on module Async and/or module Task, and each of these have a non-optional CancellationToken (the caller is forced to explicitly use CancellationToken.None or Async.DefaultCancellationToken or something else as appropriate)

    Within this context, fsharp/fslang-suggestions#1467 is intended to cover extensions to type Async, module Async and module Task wrt starting Asyncs, viz:

    You are correct that there is no specific ticket re [Value]Task.startAsync, but I think the questions in #140 (comment) can likely be resolved here before we start a long discussion

    In other words, I'd love to have an outline agreement on the full set of signatures presented in 1467 before we consider to mint a Task.startAsync ticket

    Similarly the parallelLimit/parallelDoLimit - if we can rule doing something like that in or out at this point, then a separate ticket can be done for completeness.

    Regarding the replacements of Async.StartImmediateAsTask

    I'm not proposing to replace/change anything on type Async as such (and I'm trying to keep things tractable and concrete by limiting myself to adding things to the library vs talking about adding warnings and/or having changes as such) . I did consider going down that road but as detailed in Appendix A of fsharp/fslang-suggestions#1466 IMO:

    • Async.AwaitTask cant/shouldn't be made BrowserVisible.Never as there's way too much code that's likely dependent on specifics of its quirks
    • Async.Await needs to be on the type for overloading/discovery reasons
    • I'm specifically suggesting the xmldoc for Async.Await and Async.AwaitTask will be doing most of the lifting (it's not inconceivable that something later suggests migrating off Async.AwaitTask)
    • Async.Catch and Async.catch need to coexist even if ideally people switch to a catch, with a Result result (which aligns with Task.catch
    • Async.RunSynchronously, Start, StartAsTask, StartWithContinuations, StartWithContinuationsUsingDispatchInfo, StartImmediateAsTask, StartImmediate all have a ?cancellationToken that defaults to Async.DefaultCancellationToken, which is hard to discover and can be a source of leaks in that each resulting invocation subscribes to the default CT

    So, what are you specifically suggesting we should make a ticket for? How many tickets? Are you suggesting they should be considered for the F# 11 timeframe?

  23. gusty commented on May 13, 2026

    @gusty
    Member

    Thanks for the clarification, I agree on having them in the same ticket.

  24. TheAngryByrd commented on May 13, 2026

    @TheAngryByrd
    Collaborator

    I'm really just trying to bottom out on whether the Async.withCancellation suggestion can be covered or 90% covered by some combination of smaller helper

    It probably can be handled by small helpers. Might be worth considering how this affects Fable. They don't support task and would need to shim this as transition from async -> task -> async. So having an "async native" approach might be more beneficial there.

  25. bartelink commented on May 13, 2026

    @bartelink
    MemberAuthor

    So having an "async native" approach might be more beneficial there.

    I'm not feeling sure we have proven there's a common enough pattern/need.

    It seems to me we have a variety of ways of starting tasks that admit a CT directly:- Async.RunSynchronously, Start, StartAsTask, StartWithContinuations, StartWithContinuationsUsingDispatchInfo, StartImmediateAsTask, StartImmediate

    While layering in cancellation below the top level might make sense in some cases, it feels like that belongs in with StartChild (which has a timeout option and involves LinkedCancellationTokenSource etc).

    In other words
    a) I don't feel we have enough of a handle on it to include it in this apiset so won't be attempting to address the need in 1466/7 for F# 11 timeframe
    b) I'm not convinced the ticket as it stands is helping matters even in the longer term - I suspect most people that think they want it can achieve their goals via the existing starting functions [and not having to descend into complex TaskCompletionSource juggling]

  26. bartelink commented on May 13, 2026

    @bartelink
    MemberAuthor

    @T-Gro sorry for the bombardment of atting but wanted to close out on #140 (comment)

    I'm guessing that removing Task.runAsync and having a single startAsync with the CT arg first is your preference, but the question of including Immediate in the name to:
    a) convey the semantics
    b) allow other such helpers to sit alongside either in the box or via augmentations from libraries or app preludes

    still needs answering from my perspective?

    I'm also wondering whether Async.RunTask should then more logically be called Async.RunTaskImmediate to convey that it will be using StartImmediateAsTask and not doing a thread pool or SynchronizationContext hop ?

    cc @TheAngryByrd I'm guessing you might have thoughts on the naming here too
    cc @gusty you may have less interest in this one but if you have any thoughts ...
    @gusty also I expected you might object to empty on similar grounds to singleton (= async.Zero / Task.FromResult(()) / ValueTask(())) ? (I'm considering removing it in favor of result () given Task becomes Task<unit> across the board)

    Will update 1466 and 1467 with proceedings of this and fsharp/fslang-suggestions#1466 (comment) and pop a link into the discord and the slack when I'm done with the editing later and/or tomorrow am

  27. gusty commented on May 13, 2026

    @gusty
    Member

    I'm also wondering whether Async.RunTask should then more logically be called Async.RunTaskImmediate to convey that it will be using StartImmediateAsTask and not doing a thread pool or SynchronizationContext hop ?

    Definitely yes. RunTask alone is a bit vague, given the different options available.
    Same goes for including Immediate in the other function name.

  28. bartelink commented on May 13, 2026

    @bartelink
    MemberAuthor

    Applied naming updates and guesses on changes to fsharp/fslang-suggestions#1467

  29. T-Gro commented on May 14, 2026

    @T-Gro

    I like the changes done.

    It still holds that the more technically detailed explanation must sit in API docs, and there should be an unified way of explaining common terms (a shared glossary file perharps, if I decide to land /// <include file="{share glossary xml}" path="/async-task-glossary/immediate"/> work which has a draft PR. Forcing use to use a shared glossary across Async/Task/ValueTask would ensure that same english words carry the same meaning across the API set ) :
    start / run
    immediate
    result
    catch

    As a basic test, are we able to one-sentence explain the difference between start and run ?

  30. bartelink commented on May 14, 2026

    @bartelink
    MemberAuthor

    @T-Gro A very good question

    Glossary

    Reasonably clear terms:

    • Synchronously - blocking the current OS thread (not for general use in a tree of computation)
    • Immediate - starts on current thread with no hops (which might be bad)
    • Child - shares/propagates/links cancellation token; child runs concurrently

    Less clear terms:

    Run

    • kick off somewhere and give me a handle to it
    • Alludes to Task.Run (all overloads return a Task)
    • IMO Task.Run is a common well known thing for most .NET devs

    Start

    • used in names of lots of Async things including MailboxProcessor
    • anything Async can be started to run parallel or concurrently depending on lots of factors
    • in general is intended to convey we're not doing stuff inline - we're kicking off parallel work here is the implication

    AsTask

    • Because there was no Task in FSharp.Core historically, lots of things that arguably belong on that side of the fence (and would simplify the naming and documentation of Async.(Start|Run).*) are mingled in and need disambiguation

    Survey

    Async API Might ideally be called returns notes
    RunSynchronously Async.RunBackgroundSynchronously 'T blocks, thread hop if not thread pool thread
    RunSynchronouslyImmediate (NEW) fine? 'T blocks, no thread hop
    Start Async.StartBackground unit (computation must be Async<unit>) thread pool only
    StartAsTask Task.startAsyncOpt Task<'T> admits TaskCreationOptions
    StartImmediate Async.StartImmediate unit (computation must be Async<unit>) starts immediate, then thread pool
    StartImmediateAsTask Task.startAsyncImmediate Task<'T> starts immediate, then thread pool
    StartChild Async.StartChild Async<Async<'T>> Flows CT, admits optional timeout
    StartChildAsTask Task.startAsyncChildOpt Async<Task<'T>> Flows CT, admits TaskCreationOptions
    StartWithContinuations fine? unit (computation must be Async<unit>)
    RunTaskImmediate (NEW) Async.StartTaskImmediate Async<'T> Async.CancellationToken >> Async.StartImmediateAsTask >> Async.Await

    Conclusion

    1. Start is not a useful word anywhere than in Async., where it's primary role is to identify things that instigate Async computations in various ways
    2. Run is similarly meaningless and is also overloaded (RunSynchronously is actually very similar to Task.Run >> _.Wait() semantically), but people think it runs things inline like the proposed RunSynchronouslyImmediate will do [more of].
    • In the above RunTaskImmediate is the only one using Run in the name, and, being Immediate is actually not close enough to Task.Run semantics so probably StartTaskImmediate is a better name. Also Start references StartChildAsTask / StartChild like semantics
    • Run or Start as they stand are not worth/possible to put in a glossary as they are meaningless and/or confusing. If RunSynchronouslyImmediate is the only new function with Run, there's less need to talk about the difference

    Hopefully this triggers someone to see a better naming scheme.

    For now unless there are objections, I'm leaning toward changing the proposed Async.RunTaskImmediate to Async.StartTaskImmediate to take the start vs run 'debate' off the table. I have updated RunTaskImmediate to be named StartTaskImmediate based on the above

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    feature requestNew feature or enhancement requesttopic: task-exRelated to the proposed new TaskEx library, which should get its own oss haven

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions