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 tosrc/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"
]
}
Create a src/index.res
file that logs something to the console:
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:
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.
let main = () => {
let _ = ...
}
main()
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
}
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 anint
.
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)
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(...)
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()
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()
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)
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
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!
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.")
)
}
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)
}
)
}
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.
Questions?
Please feel free to ask anything in the comments!