DEV Community

Cover image for Promises, async, and await in ReScript (with Bun!)
Josh Derocher-Vlk
Josh Derocher-Vlk

Posted on • Updated on

Promises, async, and await in ReScript (with Bun!)

ReScript is "Fast, Simple, Fully Typed JavaScript from the Future"

Let's take a look at how to use JavaScript promises, async, and await in ReScript using Bun v1 to quickly run and see our changes.

ReScript

ReScript is a strongly typed language with a JavaScript like syntax that compiles to JavaScript.

Getting set up

We'll be using Bun as our package manager and to run our code as we work.

  • If you don't already have Bun installed go ahead and run npm i bun -g.
  • Create a new folder and open up VSCode or your IDE of choice.
  • Install the ReScript extension for your IDE.
  • Set up your project with bun init. Set the entry point to src/index.mjs.
  • Install ReScript: bun install rescript@next @rescript/core

Create a bsconfig.json file to configure ReScript:

{
    "name": "bun-rescript",
    "sources": [
        {
            "dir": "src",
            "subdirs": true
        }
    ],
    "package-specs": [
        {
            "module": "es6",
            "in-source": true
        }
    ],
    "suffix": ".mjs",
    "bs-dependencies": [
        "@rescript/core"
    ],
    "bsc-flags": [
        "-open RescriptCore"
    ]
}
Enter fullscreen mode Exit fullscreen mode

Create a src/index.res file that logs something to the console:

Console.log("starting...")
Enter fullscreen mode Exit fullscreen mode

Run bun rescript build -w in one terminal tab or window and bun --watch src/index.mjs in another.

You now have ReScript quickly compiling the .res file into a .mjs file in a few milliseconds and then Bun running that code in a few milliseconds. This is a very nice quick feedback loop to use for rapid development.

Promises

I will assume you have a basic working knowledge of Promises in JavaScript.

Here's a really basic example of a Promise in ReScript:

let main = () => {
  let _ = Promise.resolve(42)->Promise.then(n => Console.log(n)->Promise.resolve)
}

main()
Enter fullscreen mode Exit fullscreen mode

Let's walk through each part of the code here.

Let's start by understanding what's going on with main and the let _ = part.

let main = () => {
  let _ = ...
}

main()
Enter fullscreen mode Exit fullscreen mode

In ReScript every line of code is an expression and the last expression in a function is the return value.

let fn = () => {
  42 // this function returns 42
}
Enter fullscreen mode Exit fullscreen mode

Note: even though we aren't adding type annotations, ReScript is able to always correctly infer the types so this function has a type signature of unit => int. unit in ReScript means that we have no value at all, so this function takes in no parameters and returns an int.

Any top level expression has to have a type of unit in ReScript, which means we can't return something in a top level function call like what we are doing with main(). So our we have to make sure it returns a type of unit and we can do that by assigning the value to _. _ is a special syntax for a value that exists but we never intend to use. If we did let x = ... the compiler would warn us that x is never used.

Creating the promise looks identical to JavaScript:

Promise.resolve(42)
Enter fullscreen mode Exit fullscreen mode

The next part is different from JS. In ReScript we don't have dot style chaining so we can't do Promise.resolve(42).then(...). ReScript has pipes, which we use with the -> operator. So we take the Promise we created and "pipe" it into the next step, which is Promise.then.

Promise.resolve(42)->Promise.then(...)
Enter fullscreen mode Exit fullscreen mode

And inside Promise.then we are logging to the console and returning the result (which is unit) as a Promise. In ReScript every Promise.then has to return another Promise. JavaScript Promises do some magic to handle returning a value or another Promise inside of .then, and since a type can only every be one thing in ReScript we have to commit to always explicitly returning a Promise. Thankfully the Promise module has a thenResolve function that can clean this up.

let main = () => {
  let _ = Promise.resolve(42)->Promise.thenResolve(n => Console.log(n))
}

main()
Enter fullscreen mode Exit fullscreen mode

Switching to async/await

This function can never throw an error so we don't need to worry about .catch so we can safely convert it to async/await syntax. If you want to avoid runtime errors you shouldn't use async/await unless you want to wrap it in a try/catch block, which can get ugly real quick.

In ReScript async/await works pretty much the same as in JavaScript. Since we're now expecting main() to return a Promise we can remove the let _ = ... part and replace it with await.

let main = async () => {
  await Promise.resolve(42)->Promise.thenResolve(n => Console.log(n))
}

await main()
Enter fullscreen mode Exit fullscreen mode

Let's make it do something

Instead of returning a static number and logging it let's take in a number and validate that it is between 0 and 100. If it's out of bounds we want to return an have it log an error.

let main = async n => {
  await Promise.resolve(n)
  ->Promise.then(n =>
    n >= 1 && n <= 100
      ? Promise.resolve(n)
      : Promise.reject(Exn.raiseError("number is out of bounds"))
  )
  ->Promise.thenResolve(n => Console.log(n->Int.toString ++ " is a valid number!"))
}

await main(10)
Enter fullscreen mode Exit fullscreen mode

We should see 10 is a valid number! in the Bun console, but we didn't properly handle the error if we give it an invalid number so we get a runtime exception.

4 | function raiseError(str) {
5 |   throw new Error(str);
            ^
error: number is out of bounds
      at raiseError
Enter fullscreen mode Exit fullscreen mode

We can improve this by using ReScript's Result type, which is a variant type that is either Ok or an Error.

let main = async n => {
  await Promise.resolve(n)
  ->Promise.thenResolve(n =>
    n >= 1 && n <= 100
      ? Ok(n->Int.toString ++ " is a valid number!")
      : Error(n->Int.toString ++ " is out of bounds")
  )
  ->Promise.thenResolve(res =>
    switch res {
    | Ok(message) => Console.log(message)
    | Error(err) => Console.error(err)
    }
  )
}

await main(1000) // => 1000 is out of bounds
await main(10) // => 10 is a valid number!
Enter fullscreen mode Exit fullscreen mode

Wrapping up

You should now have a basic understanding of how to use Promises in ReScript. The part that I think is key is that we don't have to throw errors in our promises because we have the Result type. It's a better developer and user experience to capture known errors and handle them gracefully by returning an it in an API response or rendering an error to a user in a React application.

Unknown exceptions will happen of course, but in this case we expect that the number could be invalid. What if our function was defined here and meant to used somewhere else? Let's rewrite it to return either return the number or an error message.

let validateQuantity = async n => {
  await Promise.resolve(n)->Promise.thenResolve(n =>
    n >= 1 && n <= 100
      ? Ok(n)
      : Error(n->Int.toString ++ " is out of bounds and is not a valid quantity.")
  )
}
Enter fullscreen mode Exit fullscreen mode

Now the function will return promise<result<int, string>> so anyone using this knows that we expect an error case and can handle it appropriately.

We can even make the error messages have more meaning if we change this to use pattern matching:

let validateQuantity = async n => {
  await Promise.resolve(n)->Promise.thenResolve(n =>
    switch [n >= 1, n <= 100] {
    | [false, _] => Error(n->Int.toString ++ " is less than 0 and is not a valid quantity.")
    | [_, false] => Error(n->Int.toString ++ " is greater than 100 and is not a valid quantity.")
    | _ => Ok(n)
    }
  )
}
Enter fullscreen mode Exit fullscreen mode

This would allow us to show a meaningful error message to a user if they try and do something invalid.

let validateQuantity = async n => {
  await Promise.resolve(n)->Promise.thenResolve(n =>
    switch [n >= 1, n <= 100] {
    | [false, _] => Error(n->Int.toString ++ " is less than 1 and is not a valid quantity.")
    | [_, false] => Error(n->Int.toString ++ " is greater than 100 and is not a valid quantity.")
    | _ => Ok(n)
    }
  )
}

let addToCart = async quantity => {
  let validatedQuantity = await validateQuantity(quantity)
  switch validatedQuantity {
  | Ok(n) => Console.log(n->Int.toString ++ " items successfully added to cart!")
  | Error(e) => Console.error(e)
  }
}

await addToCart(10) // => 10 items successfully added to cart!
await addToCart(1000) // => 1000 is greater than 100 and is not a valid quantity.
await addToCart(0) // => 0 is less than 1 and is not a valid quantity.
Enter fullscreen mode Exit fullscreen mode

Questions?

Please feel free to ask anything in the comments!

Top comments (10)

Collapse
 
zth profile image
Gabriel Nordeborn

Thanks for a great article!

I'd like to add two things I think is cool about async/await in ReScript:

  1. You can await directly in the switch. So, you could for example write addToCart like this:
let addToCart = async quantity => {
  switch await validateQuantity(quantity) {
  | Ok(n) => Console.log(n->Int.toString ++ " items successfully added to cart!")
  | Error(e) => Console.error(e)
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. Similarly, you mentioned that async actions that might throw needs a bunch of try catch blocks that often clutter up the code. But, there's a neat trick in ReScript allowing you to streamline this as well, inside of our beloved switch. You can actually pattern match on an exception and handle that exception right in your switch. Imagine validateQuantity could throw. You could write it like this:
let addToCart = async quantity => {
  switch await validateQuantity(quantity) {
  | Ok(n) => Console.log(n->Int.toString ++ " items successfully added to cart!")
  | Error(e) => Console.error(e)
  | exception Exn.Error(err) => Console.error(err)
  }
}
Enter fullscreen mode Exit fullscreen mode

This will compile to a try/catch in JS that will handle any async error, and at the same time letting your ReScript code stay really succinct.

Thanks again for the great article!

Collapse
 
redbar0n profile image
Magne • Edited

This was a gotcha... that in ReScript:

n->Int.toString ++ " items successfully added to cart!"
Enter fullscreen mode Exit fullscreen mode

is not equivalent to a javascript lambda..:

n -> something + somethingelse
Enter fullscreen mode Exit fullscreen mode

It was confusing that -> and => are both used in ReScript and so different. Especially when reading code on dev.to with a small font.

I wonder, since ReScript type inference is so good (it seems to know n is an int already), why can't one do n->toString instead of n->Int.toString ?

Collapse
 
jderochervlk profile image
Josh Derocher-Vlk

=> is a lambda function arrow, while -> is the pipe operator.

rescript-lang.org/docs/manual/late...
rescript-lang.org/docs/manual/late...

In the example of n->Int.toString Int is the Module that contains the toString function which converts an int into a string. It's possible to have other modules that define a toString function. ReScript can inter the value of n here since we know the type of the input of Int.toString. What it won't do it figure out what function you meant to call, which would probably be a bad thing that would lead to bugs and headaches for everyone.

Thread Thread
 
redbar0n profile image
Magne • Edited

ah, thanks. I remembered it's possible to open a specific module to avoid having to specify it everywhere:

open Int
n->toString ++ " items successfully added to cart!"
Enter fullscreen mode Exit fullscreen mode

still a bit confusing that the pipe operator -> is so similar to the lambda function arrow => though...

Collapse
 
jderochervlk profile image
Josh Derocher-Vlk

Today I learned! I am constantly impressed by how much you can do with pattern matching in this language.

Collapse
 
redbar0n profile image
Magne

I get "The value thenResolve can't be found in Promise".. hmm

Collapse
 
jderochervlk profile image
Josh Derocher-Vlk

Do you have @rescript/core installed and open?

Collapse
 
redbar0n profile image
Magne

I just tried it in the ReScript playground (to see what JS some of it outputs)... rescript-lang.org/try

Thread Thread
 
jderochervlk profile image
Josh Derocher-Vlk

In order to use Promise.thanResolve you'll need to be using @rescript/core. If you have the module open, either in a file or in bsconfig.json, you can use it like Promise.thanResolve, otherwise you will need to do RescriptCore.Promise.thanResolve.

Here is a working playground example: https://rescript-lang.org/try?version=v11.0.0-rc.3&code=PYBwpgdgBASmDOBjATgSxAFwMLGWAUADZgZQYCMUAvFAArLAC2q8YAdHvMIQG5gAUAFgBMASgC0APnpMW7DAAtIcLrwE4Iq9oWABzUfiA

Here are the settings:
Image description

Note: I am using an RC of ReScript v11, which is needed on the playground to have @rescript/core as a dependency. Core also works with ReScript v10.

Thread Thread
 
redbar0n profile image
Magne

Thank you! That was very helpful :-)