Providing JavaScript Developers Helpful Type Errors When They Call Your ReScript Library Incorrectly
Tags: javascript, rescript, types, errors
Published: 2021-04-19 17:30:13


---
title: Providing JavaScript Developers Helpful Type Errors When They Call Your ReScript Library Incorrectly 
published: true
description: Learn how to expose libraries to JavaScript developers using ReScript, and ensure they don't mess things up.
tags: JavaScript,ReScript,types,errors
canonical_url: https://jessewarden.com/2021/04/providing-javascript-developers-helpful-type-errors-when-they-call-your-rescript-library-incorrectly.html
cover_image: 
---

Introduction
------------

You can’t use [ReScript](https://rescript-lang.org/) to call ReScript incorrectly because the compiler will help you make it correct. You can’t use ReScript to call JavaScript wrong because the compiler will help you make it correct.

However, when JavaScript calls your ReScript library, it can do it incorrectly because JavaScript has no compiler; you just run it. This can ruin all the benefits of ReScript: null pointers, runtime errors, and super strange internal ReScript standard library errors that make no sense with stack traces that are not helpful.

Instead, the better thing to do is check the types at runtime, and if wrong, provide helpful runtime errors with what the JavaScript developer needs to do to fix the error. If you’re a ReScript developer providing libraries for JavaScript developers, this article is for you.

Problem
-------

You complete a cool ReScript library. You feel great. You publish your finished library for JavaScript and/or ReScript developers to use. However, a JavaScript developer using your library in their Node.js project reaches out to you, saying it doesn’t work. You respond like you do to all programming problems: “Show me your code.”

Their code looks like this:

```javascript
import { audit } from '@jesterxl/cow-audit'

audit("secret password", 23)
.then(console.log)
.catch(error => console.log("error:", error))
```


You immediately see the problems in the `audit` call. They have the parameters reversed, one is a number that’s supposed to be a string, and they’re only passing 2 parameters, not the 3 required. In ReScript, your function looks like this:

```rescript
let audit = (id:string, password:string, host:string) => {...}
```


The JavaScript developer is setup to fail in a multitude of a ways:

1. they have no idea what your function signature is besides the docs. They have no compiler to help them, and the intelli-sense is spotty. Despite that, they won’t really know if it works unless they run it; that’s how JavaScript works. Runtime errors are just “expected”.
2. You have 3 strings in a row, but no indication of *what* strings. This is why strongly typed functional developers look at functions with string parameters and think the function is un-typed. It’s easy to mess up and hard to know if you got it right.
3. JavaScript isn’t like Python; function arity (how many parameters a function takes) isn’t enforced at runtime. You can pass not enough or too many and the runtime doesn’t tell you that; it may even work.
4. The errors that occur are unpredictable. While in “ReScript Land®” things are predictable, when you have chaos calling, chaos then inevitably results.

In short, JavaScript doesn’t have types or a compiler, and the only way they know if the code works if they run it successfully or not with errors. The expectation is that the Errors will tell them what they did wrong, and they and/or the stack trace will help indicate what they did wrong and how to correct it.

That’s not how ReScript works. Null pointers and errors aren’t supposed to occur; that’s the whole point of using a soundly typed language over one that isn’t soundly typed like TypeScript. Yet here we are, having JavaScript screw things up. ReScript helps you at compile time, NOT at runtime. That’s why even if you bind to JavaScript modules or objects, while the bindings may be correct, at runtime JavaScript has no guarantee’s or perhaps you wrote the bindings wrong, and explosions ensue.

We have a communication problem. It’s now your responsibility, as a ReScript developer, to speak in a language the JavaScript developer can understand, and that’s in clear, runtime Exception messages.

Solutions
---------

There are actually 3 solutions here, the last being optional. They are using a configuration object as a single parameter, runtime type checking in ReScript, and more descriptive naming of variables indicating their type.

### Quick Note on Code Formatting

For code already written, or code we need to write, we’ll write `...` which indicates “stuff here that’s not relevant right now”.

### Single Object Parameter

A lot of JavaScript developers will sometimes use a single Object as a parameter to a function when it gets “too many parameters”. The true amount of “too many” varies. There are a variety of motivations, though, about why they use this technique to alleviate the too many. Text editors will force you to horizontally scroll to see all your parameters; no one likes horizontally scrolling except in video games. Some editors won’t provide any intelli-sense, or it will temporarily break, and so you don’t know what parameter is which. The rise of TypeScript has encouraged developers to create typed Interfaces or Types which allows typed Objects with compiler help. Some hate remembering the order, and want flexibility, especially when default values are involved.

That means, using our ReScript code above, it goes from this:

```rescript
let audit = (id:string, password:string, host:string) => {...}
```


To this:

```rescript
let audit = config => {...}
```


The id, password, and host are now names on that Object. On the ReScript side, you don’t type it, you leave it as a generic type, like a normal JavaScript Object.

This solves 2 problems:

1. The JavaScript developer cannot screw up the order; there is only 1 parameter provided. If they unknowingly provide no parameter, the function will still be invoked on the ReScript side with 1 parameter that is `undefined` “because JavaScript”.
2. The JavaScript developer knows what string goes where because they now have names on an Object, clearly indicating where they go.

However, there is one other benefit to *you* the library author, and that’s you now know exactly where they screwed up a parameter vs. they just put the wrong thing in the wrong order, and maybe the wrong type. Let’s see how that works.

### Runtime Type Checking in ReScript

Now that you have a public method with a single configuration Object being passed in, you can write some imperative looking code to inspect each variable, and if it doesn’t look correct, let the JavaScript developer know exactly which one is wrong, and how they need to fix it. Despite “being in ReScript”, we’re getting a JavaScript input, so can’t trust anything… but we CAN trust ReScript types! Therefore, we need to account for 4 things:

1. If the config itself, or a value, is `undefined`.
2. If the config itself, or a value, is `null`.
3. If a particular value is the correct type.
4. If a particular value matches our criteria for that type (i.e. String isn’t good enough)

Let’s handle those in order and how you how that looks in practice. First, we need to ensure `config` even exists; meaning something other than `undefined` or `null`. We can convert things to an `Option` using the `toOption` function in `Js.Nullable` package:

```rescript
let audit = config =>
  switch Js.Nullable.toOption(config) {
    | None => ...
    | Some(opts) => ...
```


This ensures if the JavaScript developer does something like `audit()` or `audit(wrongEmptyVariable)`, they’ll recognize where they messed up in their JavaScript calling your library function. We can now provide a more helpful error message in that scenario:

```rescript
let audit = config =>
  ...
    | None => Js.Exn.raiseError`('Your config does not exist. You need to provide an Object that looks` like { id: "23", password: "yourpass", host: "http://server.com" }')
    ...
```


Once you’ve confirmed you have an actual valid options, then we grab all of the variables, and convert them to `Options`:

```rescript
let audit = config =>
  ...
    | Some(opts) =>
      let idMaybe = Js.Nullable.toOption(opts["id"])
      let passwordMaybe = Js.Nullable.toOption(opts["password"])
      let hostMaybe = Js.Nullable.toOptions(opts["host"])
```


… and then verify each is legit or not:

```rescript
let audit = config =>
  ...
      if( Js.Option.isNone(idMaybe) ) {
        Js.Exn.raiseError(`id is not defined on your config object. It's supposed to be a non-empty string of your id, but you sent: ${idMaybe}`)
      } else if { ...
```


You repeat this for each variable, providing a helpful error. You’ll notice sometimes JavaScript developers pass the wrong thing in the wrong place “because JavaScript”, so we include what they did send above, `idMaybe` so they can see what we got on the ReScript side to help them correlate where they went wrong. The `None` in ReScript will typically print out as an empty string in JavaScript, but that pattern is helpful for JavaScript developers to see what they sent.

After you’ve verified everything, you can run additional validations, again, common mistakes JavaScript developers can make, such as empty strings, truncated strings, or strings meant for the wrong thing such as a small id and a large password.

```rescript
let audit = config =>
      } else if(Js.Option.getExn(idMaybe) === "") {
        Js.Exn.raiseError(`Your id is an empty string. It needs to be at between 2 and 7 characters, like '23', but you sent: ${idMaybe}`)
```


Notice how we’re writing some pretty imperative code which means order is important. That works in our favor because since we’ve confirmed everything in the above if statements is all the Options are in fact `Some's` then we can safely use `getExn` without fear.

Lastly, now that we’ve confirmed the JavaScript developer did everything correctly, we can call our function the normal FP way: using a bunch of parameters.

```rescript
let audit = config =>
    ...
      } else {
        _audit(Js.Option.getExn(idMaybe), Js.Option.getExn(passwordMaybe), Js.Option.getExn(hostMaybe))
```


Anything that goes wrong in `_audit` the JavaScript developer will get as a return value or resolved Promise value.

The final version looks something like this:

```rescript
let audit = config =>
  switch Js.Nullable.toOption(config) {
	| None => Js.Exn.raiseError
("Your config does not exist. You need to provide an Object that looks
 like { id: '23', password: 'yourpass', host: 'http://server.com' }")
	| Some(opts) => {
		let idMaybe = Js.Nullable.toOption(opts["id"])
		let passwordMaybe = Js.Nullable.toOption(opts["password"])
		let hostMaybe = Js.Nullable.toOptions(opts["host"])
		if(Js.Option.isNone(idMaybe)) {
			Js.Exn.raiseError(`id is not defined on your config object. It's supposed to be a non-empty string of your id, but you sent: ${idMaybe}`)
		} else if(Js.Option.isNone(passwordMaybe)) {
			Js.Exn.raiseError(`password is not defined in your config object. It's supposed to be a non-empty string, but you sent ${passwordMaybe}`)
		} else if(Js.Option.isNone(hostMaybe)) {
			Js.Exn.raiseError(`host is not defined in your config object. It's supposed to be a non-empty string, but you sent ${hostMaybe}`)
		} else if(Js.Option.getExn(idMaybe) === "") {
			Js.Exn.raiseError(`Your id is an empty string. It needs to be at between 2 and 7 characters, like '23', but you sent: ${idMaybe}`)
		} else {
			_audit(
				Js.Option.getExn(idMaybe), 
				Js.Option.getExn(passwordMaybe),
				Js.Option.getExn(hostMaybe)
			)
	  	}
	}
}
```


### Descriptive Names

They say an ounce of prevention is worth a pound of cure, and sometimes that can go a long way in dynamic languages like “[convention over configuration](https://en.wikipedia.org/wiki/Convention_over_configuration)” in Ruby. For Dynamic languages as a whole, a tactic some people use is encoding the type in the name. Some love it, some hate it. If you did that here, it’d be: `idString`, `passwordString`, and `url`.

Saying `idString` instead of `id` implies whether the id is a string or a number… it should be a string for your library.

Now `passwordString` could be further expounded to include the rules such as `password7Minimum255MaximumString`, making the JavaScript developer insecure in that maybe *they* should validate it first.

Lastly, while host is technically what we’re looking for, url is a little more correct and implies the string should start with “https://” and have some kind of domain in it. Host makes it seem like just “server.com” is ok, and we’ll magically provide the “https://” part somehow.

However, naming things is hard. Sometimes it’s easier to write validation code with helpful errors than bikeshed with yourself or team over variable names.

Conclusion
----------

Now, on the JavaScript Developer’s side, their code is changed from the dangerous:

```javascript
audit("secret password", 23)
```


To the safer and easier for both parties version:

```javascript
audit( { id: "23", password: "secret", host: "http://server.com" })
```


Any problems, the `catch` will have a helpful error message vs. the weirdness you get throwing bad things at runtime to the ReScript standard libraries & compiled JavaScript.

### Quick Note About Errors

You’ll notice I default to a Promise as that is a common coding practice in Functional Programming in ReScript, however, there are 2 types of Errors: sync and async. If your function is synchronous, then using the standard ReScript `Js.Exn.raiseError` is fine. However, if it’s asynchronous, then it’s a bit different than you may have been used too in JavaScript. In JavaScript, you’d just `return Promise.reject(new Error("reason"))` and move on, but that’s not how Promises work in ReScript. You need to create a new one inline and return that; you can’t just `Js.Promise.reject(Js.Exn.raiseError("reason"))`. It’s weird, but works; I typically put this up top:

```rescript
let reject = reason => Js.Promise.make((~resolve as _, ~reject as _) => {
  Js.Exn.raiseError(reason)
})
```

That way, if you call within a normal function, it acts like `Promise.reject` would and returns a Promise.