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?
Can you give a small example that shows this issue?
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
with returns) and then later
log_to_file raises an exception. Is the
with expression supposed to return a second time?
So promises and exceptions don’t mix well together…
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
Lwt also provides
Lwt.try_bind which you should use rather than
with to handle exceptions.
Hard to say without seeing the code. But some common ways of losing exceptions:
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
ignore instead of
async on background threads (as noted above).
cmdliner and turning an exception into an
Error value. It silently ignores the return value. e.g.
let revolt () = Error "Revolt!"
let revolt_t = Term.(const revolt $ const ())
let () = Term.exit @@ Term.eval (revolt_t, Term.info "revolt")
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).