How to set the correct result type in new Promises?

Since this is a recurring pattern I really want to understand this.

When writing promises with the new Promise library you need to make sure that the catch has the same type as the previous then.

I am trying to connect to the mongodb (which is a promise). On a success I want to write a message “connected”, and return the instance. If not successful I want to write the error.

On success the type, after resolve, is ClientInstance.t. In case of an error the type is unit.

If I understand correctly I need to create a new type, which combines both, the Ok and the Error. I create a new type using Belt.Result.t<typeWhenOk,typeWhenError>. Since the return type of the catch block is unit. I do this:

@send external connect: t => Promise.t<Belt.Result.t<ClientInstance.t,unit>> = "connect"

But that doesn’t work either. I suppose I am totally misunderstanding how types must work. What am I missing?

The error I get is:

FAILED: src/Play.cmj

  Warning number 109 (configured as error) 
  /home/hacker/rescript/rescriptjul23/src/Play.res:29:3-7

  27 │   // After resolve the type is ClientInstance.t
  28 │ })
  29 │ ->catch(e => {
  30 │   let errorMsg = switch e {
  31 │   | JsError(obj) =>

  Toplevel expression is expected to have unit type.

FAILED: cannot make progress due to previous errors.
(base) 

This is the code:

  module Mongo = {
    module ClientInstance = {
      type t                                                                                              
    }                                                                                                     
    module Client = {                                                                                     
      type t                                                                                              
      @module("mongodb") @new external make: string => t = "MongoClient"                                  
      @send                                                                                               
      external connect: t => Promise.t<Belt.Result.t<ClientInstance.t, unit>> = "connect"                 
      @send external close: t => unit = "close"                                                           
    }                                                                                                     
  }                                                                                                       
                                                                                                          
  let uri = "mongodb://localhost:27017"                                                                   
                                                                                                          
  open Promise                                                                                            
  open Mongo                                                                                              
                                                                                                          
  let client = Client.make(uri)                                                                           
                                                                                                          
  client                                                                                                  
  ->Client.connect                                                                                        
  ->then(instance => {                                                                                    
    Js.log("Connected to MongoDB")                                                                        
    // Before resolve the type is Promise.t<ClientInstance.t>                                             
    Ok(instance)->resolve
    // After resolve the type is ClientInstance.t
  })                                                                                                      
⚠ ->catch(e => {                                                                                          
    let errorMsg = switch e {                                                                             
    | JsError(obj) =>                                                                                     
      switch Js.Exn.message(obj) {                                                                        
      | Some(msg) => msg                                                                                  
      | None => "Unknown JS error"                                                                        
      }                                                                                                   
    | _ => "Some unknown non-JS error"                                                                    
    }                                                                                                     
    Js.log(errorMsg)                                                                                      
    // After resolve the type is unit                                                                     
    Error(errorMsg)->resolve                                                                              
  })                                                                   
~

What is the best way to fix this in general. What is the pattern?

Hi @el1t1st

A key concept about external types is that they should accurately reflect what the external is providing.

For example, in your code, the connect() function is declared as follows:

@send
external connect: t => Promise.t<Belt.Result.t<ClientInstance.t, unit>> = "connect"   

However the MongoDB connect() function doesn’t actually return a result type within a promise, it just returns a promise with the client instance:

@send
external connect: t => Promise.t<ClientInstance.t> = "connect"   

Once you have your external types correct, then you can focus on getting your ReScript application types correct.

Regarding the error message you’re seeing, you just need to add ->ignore to the end of your code to ignore the top level promise. You are catching the error, but still returning a resolved promise (which is correct) so that top level promise needs to be ignored.

This is common in small example code like this. In a real application within a framework that promise would likely be handled differently and would not be ignored.

Edit: I’ll add one more comment that might be helpful.

As you know, promises are used to deal with success and failure cases.
And the result type is used to deal with success and failure cases.

Depending on your needs, combining these two together may make the code more complicated.

If you return a promise that returns a result then you’ll have the following 3 cases to handle:

  1. Successful promise, OK result
  2. Successful promise, Error result
  3. Failed promise

Unless you have a use case that needs this pattern, it may be worth considering just using promises for now, and it may simplify your code a bit.

4 Likes

In the rescript-promise fetch-example you can find exactly this. The result value is not created on the bindings layer, but rather in the promise logic, like here:

then and catch in one promise chain need to resolve to the same type. Oftentimes you’d unify them as a single result type, that’s why you can see here a Ok(ret)->resolve in the then body, and a Error(msg)->resolve in the catch body.

3 Likes