Unifying Task<T> and F# MailboxProcessor exception handling

f#, mailboxprocessor

Solution

One way to handle it is have the agent return a DU with an error case. You could then raise the exception from outside the agent.

type internal IncrementResponse =
    | Response of int
    | Error of exn

type internal IncrementMessage = 
    | Increment of int * AsyncReplyChannel<IncrementResponse>

type IncrementAgent() =
    let counter = Agent.Start(fun agent -> 
        let rec loop() = 
          async { 
            let! Increment(msg, replyChannel) = agent.Receive()
            match msg with 
            | int.MaxValue -> replyChannel.Reply (Error (Failure "Boom!"))
            | _ as i -> replyChannel.Reply (Response(i + 1))
            return! loop() 
          }
        loop())

    member x.PostAndAsyncReply i =
        Async.StartAsTask (
          async {
            let! res = counter.PostAndAsyncReply (fun channel -> Increment(i, channel))
            match res with
            | Response i -> return i
            | Error e -> return (raise e)
          }
        )

Problem

When using Task<T>, an exception during the execution of the task is thrown during Task.Wait(); when using F#'s MailBoxProcessor, the exception gets swallowed and needs to be explicitly dealt with as per this question. This difference makes it difficult to expose F# agents to C# code via a Task. For example, this agent: ``` type internal IncrementMessage = Increment of int * AsyncReplyChannel<int> type IncrementAgent() = let counter = Agent.Start(fun agent -> let rec loop() = async { let! Increment(msg, replyChannel) = agent.Receive() match msg with | int.MaxValue -> return! failwith "Boom!" | _ as i -> replyChannel.Reply (i + 1) return! loop() } loop()) member x.PostAndAsyncReply i = Async.StartAsTask (counter.PostAndAsyncReply (fun channel -> Increment(i, channel))) ``` can be called from C#, but the exception is not returned to C#: ``` [Test] public void ExceptionHandling() { // // TPL exception behaviour // var task = Task.Factory.StartNew<int>(() => { throw new Exception("Boom!"); }); try { task.Wait(); } catch(AggregateException e) { // Exception available here Console.WriteLine("Task failed with {0}", e.InnerException.Message); } // // F# MailboxProcessor exception behaviour // var incAgent = new IncrementAgent(); task = incAgent.PostAndAsyncReply(int.MaxValue); try { task.Wait(); // deadlock here } catch (AggregateException e) { Console.WriteLine("Agent failed with {0}", e.InnerException.Message); } } ``` Instead of getting the exception, the C# code just hangs at task.Wait(). Is there any way to get the F# agent to behave like a Task? If not, it seems like there is limited use in exposing F# agents to other .NET code.

Original source

Related problems