It’s pretty difficult to do anything reasonable with Js.Promise.catch. Theoretically, you can write a function that checks the runtime shape of the caught object and then magically casts it to your exception type, but that’s also not good because it depends on the implementation details.
The best option I know of for handling async errors in ReScript is to use the result type. I made a wrapper library some time ago to help with that, it’s a bit outdated now from current ReScript style (pipe-first) but maybe it can give you some ideas: https://github.com/yawaramin/prometo/blob/main/src/Yawaramin__Prometo.rei
Ah, that makes me feel less insane Thanks for the tips. Seems like Js.Promise is not terribly useful if we cant model for common (error) cases. I question why include it’s in the stdlib at all if doesn’t do something result-y . Not having pragmatic error handling makes the module moot, no?
In case of my originally proposed ryyppy/rescript-promise binding, handling specific ReScript exceptions would look something like this:
exception MyError(string)
open Promise
Promise.reject(MyError("test"))
->then(str => {
Js.log("this should not be reached: " ++ str)
// Here we use the builtin `result` constructor `Ok`
Ok("successful")->resolve
})
->catch(e => {
let err = switch e {
| MyError(str) => "found MyError: " ++ str
| _ => "Some unknown error"
}
// Here we are using the same type (`t<result>`) as in the previous `then` call
Error(err)->resolve
})
->then(result => {
let msg = switch result {
| Ok(str) => "Successful: " ++ str
| Error(msg) => "Error: " ++ msg
}
Js.log(msg)
resolve()
})
->ignore
Thanks all. Given that my promises are created and resolved in an abstraction, I want error states represented by either:
My Err type (WitErr.t), derived from a passed function, or
Js.Exn.t, implicitly, because JS Runtimes may just fail with this
Using inspiration from above, I created the following in my abstraction:
type domain_error<'t> =
| ExternalError(Js.Exn.t)
| InternalErr('t)
The abstraction maps my known error cases into InternalError(WitErr.t). In my Promise.catch, I safely handle the case where JS Runtimes throw with who knows what, mapping that gracefully into a Js.Exn.t:
Js.Promise.catch(exn => {
let err = exn->%raw("(x => x instanceof Error ? x : new Error(String(x)))")
setErr(_ => Some(ExternalError(err)))
Js.Promise.reject(err)
})
Once the promise is fulfilled, my abstraction reads a state container derived of the promise container, not the fulfilled promise container directly.
// let queryResult = useQuery(~queryFn=myQueryThatUsesPromises)
switch queryResult.error {
| Some(ExternalError(e)) => () // e is Js.Exn.t
| Some(InternalErr(e)) => () // e is WitErr.t
| _ => ()
}