Handling exceptions thrown in promises

Last week I was trying to figure out why @bs.open is necessary to have at all I did a little testing in isolation, and here’s what I found:

  • When an OCaml exception is thrown from within a synchronous function, it can be caught by pattern match, as follows:
switch (thingThatThrowsOCaml()) {
  | blah => ...
  | exception SomeCustomExn(error) => ... here you can do something with the ocaml exception.
}

That works well!

But now take that same code, and put it into a promise chain:

thingThatThrowsAnOCamlExnInsideOfAJSPromise()
->Js.Promise.catch(err => {
  switch (err) {
    | someJsError => ...branch A
    | exception SomeCustomExn(error) => ... branch B
  }
})

In that case, branch A is always taken. The ocaml exception is no longer recognized. I have to use @bs.open on err instead of just a switch to detect the ocaml exception in that case.

I’ve heard it argued that this is for type safety, but I don’t see (from a developer’s perspective), how putting the switch that matches exception ... inside of a promise callback would be any less type safe than using it outside of a promise. Branch B in the switch expression would still only match values of that actual type.

This feels like it might be a bit of a hole in Bucklescript to me. Is the logic required to extract OCaml expressions like this without the decorator just too complex?

On a related note, @austindd posted a follow up to my questions in discord: https://discord.com/channels/235176658175262720/235176658175262720/768225829356175390 (thanks, Austin).

The specific case covered there looks useful when I have an exn already, and I’m trying to get a Js.Exn out of it, but that’s not what I’m doing here. I usually have to take a Js.Promise.error and try to turn it into something useful. It’d be amazing if I could use one switch expression to handle the three possible cases of 1) JS Exn, 2) Ocaml exn, or 3) some other value. But currently the code to do that is pretty ugly, not to mention that it requires some obj.magic since there’s no way to convert a Js.Promise.error to a Js.Exn.

Think there’s anything we could do to improve the future ergonomics here, Hongbo and @chenglou? I know that right now my error-handling code is definitely not a pit of success. I usually end up wanting to avoid error handling, since I’m almost always dealing with JS promises, and I almost always have to deal with the difficulties above.

Thanks!

1 Like

@mrmurphy
There are definitely some things that could be done to make this easier. One way to handle this is to create helper functions to convert a Js.Promise.t into something more useful to you. Probably something like this:

type taggedError =
  | Exn(exn)
  | JsExn(Js.Exn.t)
  | String(string)

let classifyError: Js.Promise.error => option(taggedError) = // ...

Treating Js.Promise.error as if it were a Js.Exn.t without checking it at runtime could throw errors, but perhaps that is acceptable for your use case. If you really want to cast it to Js.Exn.t or exn, then I would recommend defining a particular function which does only that, rather than using Obj.magic directly. The reason is that Obj.magic can lead to false positives for the type checker, and you might not be passing the correct type after refactoring the code later. You can even define a new module which extends the Js.Promise module to include this type-casting function.

Another solution is to use an alternative promise library that transforms Js.Promise.t('a) into something like Future.t(result('a, 'e)), which will capture the error type in the signature and provide the data to your map/then handler as a result('a, 'e) type that can be pattern-matched.

It really depends on what you expect your promise rejection errors to look like, so there is not a one-size-fits-all solution to this problem. JS promises are inherently unsafe at the type level, and it’s not that far off from OCaml’s exception semantics… it’s impossible to know the exact shape of an exception until you test that shape at runtime.

That said, if you’re interacting with a well-documented API, then you might actually know the exact type/shape of the error, and a simple type-cast function will be perfectly fine.

1 Like

Great follow up, @austindd. And great points. Thanks for taking the time to write out that thoughtful answer!

1 Like