Promises, async, and await in ReScript (with Bun!)
Tags: rescript, javascript, bunjs, tutorial
Published: 2023-09-26 16:42:18

_[ReScript is "Fast, Simple, Fully Typed JavaScript from the Future"](https://rescript-lang.org/)_

Let's take a look at how to use JavaScript promises, async, and await in ReScript using [Bun v1](https://bun.sh/blog/bun-v1.0) 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](https://rescript-lang.org/docs/manual/latest/editor-plugins) 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: 
```json
{
    "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"
    ]
}
```

Create a `src/index.res` file that logs something to the console:
```rescript
Console.log("starting...")
```
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:
```rescript
let main = () => {
  let _ = Promise.resolve(42)->Promise.then(n => Console.log(n)->Promise.resolve)
}

main()
```

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.
```rescript
let main = () => {
  let _ = ...
}

main()
```

In ReScript every line of code is an expression and the last expression in a function is the return value.
```rescript
let fn = () => {
  42 // this function returns 42
}
```
> 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:
```rescript
Promise.resolve(42)
```

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`.
```rescript
Promise.resolve(42)->Promise.then(...)
```

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.

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

main()
```

## 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`.

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

await main()
```

## 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.

```rescript
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)
```

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
```

We can improve this by using ReScript's [`Result`](https://rescript-lang.org/docs/manual/latest/api/belt/result) type, which is a [variant type](https://rescript-lang.org/docs/manual/latest/variant) that is either `Ok` or an `Error`.

```rescript
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!
```

## 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.

```rescript
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.")
  )
}
```

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:

```rescript
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)
    }
  )
}
```

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

```rescript
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.
```

## Questions?
Please feel free to ask anything in the comments!