A couple of times now I’ve tried debugging some Lwt code which has ended up being that a function has raised an exception and it has been handled silently.
From what my mental model would say is that using just the Lwt library, if an exception was raised in a call that it would then be raised all the way to the top level (barring a try statement).
The issue in these sections is that there aren’t any try statements in these sections.
One option that I’ve just thought of is that if somewhere else in the code there is a try statement, and an 'a Lwt evaluates to an exception while it is waiting, that that could be captured?
Regardless is there any way to definitively avoid this occurring in the future?
There are fundamental difficulties with handling exceptions in concurrent programs, and maybe you stumbled upon one of them. Give an example for more specific feedback but below is some explanations about the above statement (which maybe will help you) as well as some pointers.
let log_to_file (fname : string) (msg : string) : unit Lwt.t = …
let work () =
…
…
ignore (log_to_file "/tmp/mylogs" "no time to wait");
…
This case is just one specific example of a more general pattern: you set up some work to complete asynchronously, but you do not have time/reason to wait for the result.
In this case, if work returns before log_to_file raises an exception, “where” should the exception be raised. More specifically, consider try work () with _ -> … where work returns (and hence the whole try-with returns) and then later log_to_file raises an exception. Is the try-with expression supposed to return a second time?
So promises and exceptions don’t mix well together…
Lwt provides Lwt.async : unit Lwt.t -> unit which has a misleading name but is specifically made to help you with the above. You first set an exception hook with Lwt.async_exception_handler := handler and then you wrap your can’t-wait-for-result side-effecting calls with Lwt.async (fun () -> <expr>) and exceptions raised in <expr> will be passed to handler.
Lwt also provides Lwt.fail/Lwt.try_bind which you should use rather than raise/try-with to handle exceptions.
Hard to say without seeing the code. But some common ways of losing exceptions:
Using Lwt.join toplevel_threads to run several top-level loops that should never return. It won’t report the exception until they all fail. Use Lwt.choose instead.
Using ignore instead of async on background threads (as noted above).
Using cmdliner and turning an exception into an Error value. It silently ignores the return value. e.g.
#require "cmdliner";;
let revolt () = Error "Revolt!"
open Cmdliner
let revolt_t = Term.(const revolt $ const ())
let () = Term.exit @@ Term.eval (revolt_t, Term.info "revolt")
Using logs and turning the exception into an error message, when the application didn’t configure logging.
I think that the join is most likely to be the issue, because I’ve never been able to reproduce it in toy examples (bc I wasn’t joining several top-level loops).