Match & map Js.Promise.error to something sensible

Hi all,

I want to catch Js.Promise.error's, but .error type is not a container, e.g. error<t>, thus I cannot figure out how to do anything meaningful with it.

|> Js.Promise.catch(err => { 
   switch err {
     | v => j` ${Obj.magic(v)}` // v is type Js.Promise.error
   }
  // ...
})

But wouldn’t you know, in my application, err is actually a

exception FailedRequestJson(Fetch.Response.t)

which is my own error type. I know this, because I Js.Log'd err in my actual source ({ RE_EXN_ID: "Exn.FailedRequestJson/2", _1: Response }).

I cannot figure out how to introspect and map this type to the actual value.

switch err {
  // | Js.Exn(ex) => ... // nope
  // | Exn.FailedRequestJson(_) => ... // nope
  | v => j` ${Obj.magic(v)}` // obviously not great
} 

Some coaching would be helpful! :slight_smile:

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

2 Likes

Ah, that makes me feel less insane :grinning: 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 :slight_smile:. 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

This is possible because the catch binding is actually doing some runtime classification between ReScript exceptions and JS exceptions. Only tricky part is if you want to classify a particular kind of JS exception, but this was not the question here to begin with.

3 Likes

Thanks all. Given that my promises are created and resolved in an abstraction, I want error states represented by either:

  1. My Err type (WitErr.t), derived from a passed function, or
  2. 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
| _ => ()
}
1 Like