There are several ways to change a unit Lwt.t into a unit.
Lwt.dont_wait is like async but you also provide an exception handler
Lwt.async the exception handler is set in a global reference and so depending on your application it might or might not be ok
ignore doesn’t provide any helper for exception management
let _ : unit Lwt.t = … in … is similar to ignore but doesn’t change the type it just doesn’t bind it
I’m not sure what you mean by elegant or smell, but you can have your pick of the existing solutions above, or you can even roll something more specific to your needs (e.g., maintain a global list of ignored promises that you occasionally poll for errors or something of this sort). Generally though, “the type system tells me to convert this promise into an immediate unit” is not often a good reason to do so; so I’d recommending thinking about why you don’t need to wait for the promise to resolve, what happens to the work carried out by that promise, and why is it you need a unit in the first place.
In this case, you can use a simpler synchronisation primitive than an mvar. The mvar mechanism is a good way to synchronise different promises, some waiting for input and other producing that input. But the mvar mechanism is a bit richer than that and it also allows the producer to wait for an input having been taken by a consumer. That’s why put returns a promise.
A simpler synchronisation primitive is the one-shot waiter-wakener: Lwt.wait. This primitive is simpler (it’s one-shot!) and as a consequence, the writer doesn’t need to wait ever: the return type of Lwt.wakeup is unit.
let read_full response_body : String.t Lwt.t =
let is_done, notify_is_done: unit Lwt.t = Lwt.wait () in
let collector : string List.t ref = ref  in
let on_eof () : unit = Lwt.wakeup notify_is_done in
let rec on_read bs ~off ~len : unit =
collector := (Bigstringaf.substring ~off ~len bs) :: !collector;
Httpaf.Body.schedule_read response_body ~on_read ~on_eof
Httpaf.Body.schedule_read response_body ~on_read ~on_eof;
let* () = is_done in
let collected = String.concat "" (List.rev !collector) in
Lwt.return collected ;;
In addition to changing an mvar into a waiter-wakener pair, there is also a small difference where the synchronisation channel just sends unit instead of sending the result. The result is computed afterwards. This is optional, you could switch it back the way it was. I wrote is this way to separate the synchronisation concern from the computing of the result concern. But merging the two is ok too.