Skip to content

TryFinallyAsync implementation ignores potential exceptions in TryFinally #16189

@En3Tho

Description

@En3Tho

Currently implementation of TryFinallyAsync is defined as:

member inline internal this.TryFinallyAsync
        (
            body: TaskCode<'TOverall, 'T>,
            compensation: unit -> ValueTask
        ) : TaskCode<'TOverall, 'T> =
        ResumableCode.TryFinallyAsync(
            body,
            ResumableCode<_, _>(fun sm ->
                if __useResumableCode then
                    let mutable __stack_condition_fin = true
                    let __stack_vtask = compensation ()

                    if not __stack_vtask.IsCompleted then
                        let mutable awaiter = __stack_vtask.GetAwaiter()
                        let __stack_yield_fin = ResumableCode.Yield().Invoke(&sm)
                        __stack_condition_fin <- __stack_yield_fin

                        if not __stack_condition_fin then
                            sm.Data.MethodBuilder.AwaitUnsafeOnCompleted(&awaiter, &sm)

                    __stack_condition_fin
                else
                    let vtask = compensation ()
                    let mutable awaiter = vtask.GetAwaiter()

                    let cont =
                        TaskResumptionFunc<'TOverall>(fun sm ->
                            awaiter.GetResult() |> ignore
                            true)

                    // shortcut to continue immediately
                    if awaiter.IsCompleted then
                        true
                    else
                        sm.ResumptionDynamicInfo.ResumptionData <- (awaiter :> ICriticalNotifyCompletion)
                        sm.ResumptionDynamicInfo.ResumptionFunc <- cont
                        false)
        )

Note that it never calls awaiter.GetResult() meaning that if ValueTask was compelted synchronously it will throw but if not then it will just ignore result of the async computation.

Proposed fix is (matches .Bind implementation for example):

member inline internal this.TryFinallyAsync
        (
            body: TaskCode<'TOverall, 'T>,
            compensation: unit -> ValueTask
        ) : TaskCode<'TOverall, 'T> =
        ResumableCode.TryFinallyAsync(
            body,
            ResumableCode<_, _>(fun sm ->
                if __useResumableCode then
                    let mutable __stack_condition_fin = true
                    let __stack_vtask = compensation ()
                    let mutable awaiter = __stack_vtask.GetAwaiter()
                    
                    if not awaiter.IsCompleted then                        
                        let __stack_yield_fin = ResumableCode.Yield().Invoke(&sm)
                        __stack_condition_fin <- __stack_yield_fin

                    if __stack_condition_fin then
                        awaiter.GetResult()
                    else
                        sm.Data.MethodBuilder.AwaitUnsafeOnCompleted(&awaiter, &sm)

                    __stack_condition_fin
                else
                    let vtask = compensation ()
                    let mutable awaiter = vtask.GetAwaiter()

                    let cont =
                        TaskResumptionFunc<'TOverall>(fun sm ->
                            awaiter.GetResult() |> ignore
                            true)

                    // shortcut to continue immediately
                    if awaiter.IsCompleted then
                        cont.Invoke(&sm)
                    else
                        sm.ResumptionDynamicInfo.ResumptionData <- (awaiter :> ICriticalNotifyCompletion)
                        sm.ResumptionDynamicInfo.ResumptionFunc <- cont
                        false)
        )

Test code is:

[<Fact>]
let``task should propagate exception if thrown in DisposeAsync``() = task {
    let disposable = { new IAsyncDisposable with
        member _.DisposeAsync() = ValueTask(task {
            do! Task.Delay(1)
            failwith "boom"
        })
    }

    let! _ = Assert.ThrowsAsync<Exception>(fun () -> task {
        use _ = disposable
        ()
    })
    ()
}

Current behavior:

Assert.Throws() Failure
Expected: typeof(System.Exception)
Actual:   (No exception was thrown)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Area-Compiler-StateMachinesSequence, list, task and other state machine compilationBreaking-changeDescribes a bug which is also a breaking change.BugImpact-Medium(Internal MS Team use only) Describes an issue with moderate impact on existing code.

    Type

    Projects

    Status

    New

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions