Lwt.catch does not seem to do what I expect

I tried using http-lwt-client to make a simple get request

let+ response = Http_lwt_client.one_request ~meth:`GET (Uri.to_string uri) in

which results in an error

Fatal error: exception File "src/happy_eyeballs.ml", line 185, characters 5-11: Assertion failed
Raised at Happy_eyeballs.timer in file "src/happy_eyeballs.ml", line 185, characters 5-26
Called from Happy_eyeballs_lwt.timer.loop in file "lwt/happy_eyeballs_lwt.ml", line 117, characters 22-56
Called from Lwt.Sequential_composition.bind.create_result_promise_and_callback_if_deferred.callback in file "src/core/lwt.ml", line 1860, characters 23-26

let%lwt doesnā€™t change the error

Then I tried wrapping the call in an Lwt.catch - I would have expected that Lwt.catch would catch the exception, allowing me to get a more detailed backtrace:

    let+ response =
      Lwt.catch
        (fun () -> Http_lwt_client.one_request ~meth:`GET (Uri.to_string uri))
        (fun e -> Lwt.return_error (`Unknown (sexp_of_exn e)))
    in

but the catch is ignored - the code never executes and the app crashes


  • How is Lwtā€™s catch different from JavaScriptā€™s Promise.catch and is there another equivalent - I want to catch exceptions from a Lwt call, so that the whole app doesnā€™t crash? For example this JavaScript code works as expected - the catch is called
  const response = await fetch('https://invalid').catch(e => {
      console.log('err', e)
  })
  • How to debug and get proper traces from Lwt promises? the raised Fatal error is not helpful - it shows that an error occurred in a file that is a dependency of a library that I use; to connect the dots in a bigger context is simply not possible. I want to get the same stack traces that exist in other languages
1 Like

Can you show the crash? Is there any output from the crash or does the app just silently exit? Can you log a message from the exception handler so you know the control flow reaches it?

It looks like youā€™re hitting this bug: An `assert false` can be reach Ā· Issue #17 Ā· roburio/happy-eyeballs Ā· GitHub

As to why it crashes instead of being caught by Lwt.catch: There is an Lwt.async somewhere in your dependencies that creates the promise that causes the failed assertion, so the exception ends up in Lwt.async_exception_hook.

1 Like

Can you show the crash? Is there any output from the crash or does the app just silently exit?

It is the error I posted: Fatal error: exception File...

Can you log a message from the exception handler so you know the control flow reaches it?

I did try to log a message from the exception handler (thatā€™s how I found it wasnā€™t reached)

    Stdio.print_endline "___________ start";
    let+ response =
      Lwt.catch
        (fun () -> Http_lwt_client.one_request ~meth:`GET (Uri.to_string uri))
        (fun e ->
          Stdio.print_endline "REACHED";
          Lwt.return_error (`Unknown (sexp_of_exn e)))
    in
    Stdio.print_endline "end ___________";

thanks @quernd I am not sure I understand what this means though,
I have Lwt.async code in my code base as well, why is this a cause of the failed assertion?

Lwt.async in itself is not the cause of the failed assertion, but the assert false happens in a promise created through Lwt.async (Iā€™m guessing here).

The danger with Lwt.async is that whenever such a promise is rejected, the exception is not handled locally, but in a global exception handler, whose default behavior is to just print the exception and terminate: Module Lwt

1 Like

In general, you should avoid async. Itā€™s occasionally useful but it often lead to issues: exceptions are ā€œdetachedā€ from the normal control-flow of your program, asynced promises can be left still unresolved when your main resolves and your program exits. So the general recommendations would be

  1. Try to program without them. You can often hold onto the promise and join at a reasonable point later. And if you canā€™t then you may still be able to register the promises in some global ā€œdetached promiseā€ collection which you can handle from your main.

  2. If you still need detached promises, then instead of Lwt.async (which uses a global mutable exception handler) you should use Lwt.dont_wait (which takes an explicit exception handler at call-site).

5 Likes

thanks Raphael, unfortunately this specific error resulted from a call to a library, so it seems I donā€™t have direct control in my application code to prevent that from happening since if I understand correctly - there isnā€™t a catch all function that captures async exceptions and prevents them from bubbling and crashing Lwt.run ?

So in general Iā€™m hoping to understand how to get more detailed errors originating from Lwt, because currently an error that points to a file, that is not even in my direct dependencies is not useful (and was difficult to find where in my whole application this even originates from)

  1. Try to program without them. You can often hold onto the promise and join at a reasonable point later. And if you canā€™t then you may still be able to register the promises in some global ā€œdetached promiseā€ collection which you can handle from your main.

my concrete use case is for things like optional notifications, so there isnā€™t a good place to join promises - I want to trigger an event and return control back to the caller as quickly as possible

  1. If you still need detached promises, then instead of Lwt.async (which uses a global mutable exception handler) you should use Lwt.dont_wait (which takes an explicit exception handler at call-site).

Thanks, thatā€™s a good idea and iā€™ll definitely replace the calls I have control over!

1 Like

There is such a ā€œcatch-allā€ exception handler, itā€™s called Lwt.async_exception_hook. Itā€™s a reference that you can set to a different implementation that doesnā€™t terminate your program: Module Lwt

2 Likes

sorry, I donā€™t mean a catch all that deals with unhandled exceptions - I donā€™t mind these crashing the app - I wouldnā€™t know what to do with them anyways

I mean a catch that captures exceptions originating from a specific function call

For example this JavaScript code calls a function thirdPartyFnCall and catches errors that only originate from it (even though we are not waiting for the promise):

const delay = (timeout: number) => new Promise(r => setTimeout(r, timeout))
const thirdPartyFnCall = async () => {
    await delay(100)
    throw new Error('a.err')
}
const b = async () => {
    thirdPartyFnCall().catch(e => console.log('caught a error', e))
    return 99
}
const main = async () => {
    const res = await b();
    console.log('res', res);
    await delay(200);
    console.log('END');
}
main();

Playground Link (click ā€˜Runā€™)

Such a catch does not exist in Lwt. Although Iā€™m not sure exactly what such a catch function would be because Iā€™m not sure what ā€œoriginating from a function callā€ means.

In Lwt, the code equivalent to yours is

open Lwt.Syntax

let thirdPartyFnCall () =
  let* () = Lwt_unix.sleep 0.1 in
  raise Exit

let b () =
  Lwt.dont_wait
    thirdPartyFnCall
    (fun exc -> Printf.printf "caught a error %s\n" (Printexc.to_string exc)) ;
  99

let main () =
  let res = b () in
  Printf.printf "res %d\n" res;
  let* () = Lwt_unix.sleep 0.2 in
  Printf.printf "END\n";
  Lwt.return ()

let () = Lwt_main.run (main ())
1 Like