MailboxProcessor and exceptions
Asked Answered
H

2

18

I wonder, why MailboxProcessor's default strategy of handling exceptions is just silently ignore them. For example:

let counter =
    MailboxProcessor.Start(fun inbox ->
        let rec loop() =
            async { printfn "waiting for data..."
                    let! data = inbox.Receive()
                    failwith "fail" // simulate throwing of an exception
                    printfn "Got: %d" data
                    return! loop()
            }
        loop ())
()
counter.Post(42)
counter.Post(43)
counter.Post(44)
Async.Sleep 1000 |> Async.RunSynchronously

and nothing happens. There is no fatal stop of the program execution, or message box with "An unhandled exception" arises. Nothing.

This situation becomes worse if someone uses PostAndReply method: a guaranteed deadlock as the result.

Any reasons for such behavior?

Hilda answered 29/5, 2012 at 18:58 Comment(0)
L
6

I think the reason why the MailboxProcessor in F# does not contain any mechanism for handling exceptions is that it is not clear what is the best way for doing that. For example, you may want to have a global event that is triggered when an unhandled exception happens, but you may want to rethrow the exception on the next call to Post or PostAndReply.

Both of the options can be implemented based on the standard MailboxProcessor, so it is possible to add the behaviour you want. For example, the following snippet shows HandlingMailbox that adds a global exception handler. It has the same interface as normal MailboxProcessor (I omitted some methods), but it adds OnError event that is triggered when an exception happens:

type HandlingMailbox<'T> private(f:HandlingMailbox<'T> -> Async<unit>) as self =
  let event = Event<_>()
  let inbox = new MailboxProcessor<_>(fun inbox -> async {
    try 
      return! f self
    with e ->
      event.Trigger(e) })
  member x.OnError = event.Publish
  member x.Start() = inbox.Start()
  member x.Receive() = inbox.Receive()
  member x.Post(v:'T) = inbox.Post(v)
  static member Start(f) =
    let mbox = new HandlingMailbox<_>(f)
    mbox.Start()
    mbox

To use it, you would write the same code as what you wrote before, but you can now handle exceptions asynchronously:

let counter = HandlingMailbox<_>.Start(fun inbox -> async {
  while true do 
    printfn "waiting for data..." 
    let! data = inbox.Receive() 
    failwith "fail" })

counter.OnError.Add(printfn "Exception: %A")
counter.Post(42) 
Lagas answered 29/5, 2012 at 19:58 Comment(3)
Yes, it can be implemented, of course. I don't understand, why default behavior is so speechless. It can re-throw exception in case of there is no handlers in MailboxProcessor.add_Error, for example. It's difficult to debug Async/multithreaded code. Why should we make this task even harder?Hilda
You can argue that it would be better to take down the process (unhandled exception) if no handlers have been attached. I don't recall the rationale.Uproar
One problem with rethrowing exceptions is that you lose the stack trace - for example async{failwith "bad"} gives a stack trace pointing deep in the F# libs, which can be annoying debugging - apparently this is a .NET limitation on rethrowing exceptions on a different threadStreetman
U
17

There is an Error event on the MailboxProcessor.

http://msdn.microsoft.com/en-us/library/ee340481

counter.Error.Add(fun e -> printfn "%A" e)

Of course, you can do something like Tomas' solution if you want to exert fine control yourself.

Uproar answered 29/5, 2012 at 20:24 Comment(3)
Yes, I know about this member Error. I wonder why default on Error handler does not do anything. It can re-throw, or write to stderr, or halt application -- anything is better than just silently ignore exceptions.Hilda
Doh! I was looking for an existing event on standard MailboxProcessor, but somehow I completely missed the Error event...Lagas
Note that you can also raise the exception instead of printing it, if you want your program to crash when a MailboxProcessor fails instead of continuing to run in a potentially-broken state.Peter
L
6

I think the reason why the MailboxProcessor in F# does not contain any mechanism for handling exceptions is that it is not clear what is the best way for doing that. For example, you may want to have a global event that is triggered when an unhandled exception happens, but you may want to rethrow the exception on the next call to Post or PostAndReply.

Both of the options can be implemented based on the standard MailboxProcessor, so it is possible to add the behaviour you want. For example, the following snippet shows HandlingMailbox that adds a global exception handler. It has the same interface as normal MailboxProcessor (I omitted some methods), but it adds OnError event that is triggered when an exception happens:

type HandlingMailbox<'T> private(f:HandlingMailbox<'T> -> Async<unit>) as self =
  let event = Event<_>()
  let inbox = new MailboxProcessor<_>(fun inbox -> async {
    try 
      return! f self
    with e ->
      event.Trigger(e) })
  member x.OnError = event.Publish
  member x.Start() = inbox.Start()
  member x.Receive() = inbox.Receive()
  member x.Post(v:'T) = inbox.Post(v)
  static member Start(f) =
    let mbox = new HandlingMailbox<_>(f)
    mbox.Start()
    mbox

To use it, you would write the same code as what you wrote before, but you can now handle exceptions asynchronously:

let counter = HandlingMailbox<_>.Start(fun inbox -> async {
  while true do 
    printfn "waiting for data..." 
    let! data = inbox.Receive() 
    failwith "fail" })

counter.OnError.Add(printfn "Exception: %A")
counter.Post(42) 
Lagas answered 29/5, 2012 at 19:58 Comment(3)
Yes, it can be implemented, of course. I don't understand, why default behavior is so speechless. It can re-throw exception in case of there is no handlers in MailboxProcessor.add_Error, for example. It's difficult to debug Async/multithreaded code. Why should we make this task even harder?Hilda
You can argue that it would be better to take down the process (unhandled exception) if no handlers have been attached. I don't recall the rationale.Uproar
One problem with rethrowing exceptions is that you lose the stack trace - for example async{failwith "bad"} gives a stack trace pointing deep in the F# libs, which can be annoying debugging - apparently this is a .NET limitation on rethrowing exceptions on a different threadStreetman

© 2022 - 2024 — McMap. All rights reserved.