A lightweight, type-safe binding to JS promises:
Js.log(Promise.resolved("Hello")); /* Promise { 'Hello' } */
Promise.resolved("Hello")
->Promise.map(s => s ++ " world!")
->Promise.get(s => Js.log(s)); /* Hello world! */
2310: syntax error, consider adding a `;' before
As you can see on the first line, Promise.t
maps directly to familiar JS
promises from your JS runtime. That means...
reason-promise
directly to write JS bindings.reason-promise
.reason-promise
still binds to
it!There is only one exception to the rule that Promise.t
maps directly to JS
promises: when there is a promise nested inside another promise. JS breaks the
type safety of promises in a misguided attempt to
disallow nesting. reason-promise
instead emulates it in a way that makes
promises type-safe again. This is in contrast to BuckleScript's
built-in Js.Promise
, which directly exposes the JS behavior, and so is not
type-safe.
In addition:
reason-promise
offers a clean functional API, which replaces rejection with
helpers for Result
and Option
.reason-promise
is tiny. It weighs in at about 1K bundled.reason-promise
also has a full, standalone pure-Reason
implementation, which passes all the same tests. It can be used for
native code or in JS.Result
reason-promise
makes promises type-safenpm install reason-promise
Then, add reason-promise
to your bsconfig.json
:
{
"bs-dependencies": [
"reason-promise"
]
}
To quickly get a project for pasting the code examples, clone the
example repo. The code is in main.re
.
git clone https://github.com/aantron/promise-example-bsb
cd promise-example-bsb
npm install
npm run test # To run each example.
There it also an example repo with a trivial binding to parts of node-fetch.
While reading the tutorial, it can be useful to glance at the type signatures of the functions from time to time. They provide a neat summary of what each function does and what it expects from its callback.
The most basic function for creating a new promise is
Promise.pending
:
let (p, resolve) = Promise.pending();
Js.log(p); /* Promise { <pending> } */
let (p,resolve) = Promise.pending ()
let _ = Js.log p
The second value returned, resolve
, is a function for resolving the promise:
let (p, resolve) = Promise.pending();
resolve("Hello");
Js.log(p); /* Promise { 'Hello' } */
let (p,resolve) = Promise.pending ()
let _ = resolve "Hello"
let _ = Js.log p
Promise.resolved
is a helper that returns an already-resolved
promise:
let p = Promise.resolved("Hello");
Js.log(p); /* Promise { 'Hello' } */
let p = Promise.resolved "Hello"
let _ = Js.log p
...and Promise.exec
is for wrapping functions that take callbacks:
[@bs.val]
external setTimeout: (unit => unit, int) => unit = "setTimeout";
let p = Promise.exec(resolve => setTimeout(resolve, 1000));
Js.log(p); /* Promise { <pending> } */
/* Program then waits for one second before exiting. */
external setTimeout : (unit -> unit) -> int -> unit = "setTimeout"[@@bs.val ]
let p = Promise.exec (fun resolve -> setTimeout resolve 1000)
let _ = Js.log p
To do something once a promise is resolved, use Promise.get
:
let (p, resolve) = Promise.pending();
p->Promise.get(s => Js.log(s));
resolve("Hello"); /* Prints "Hello". */
2310: syntax error, consider adding a `;' before
Use Promise.map
to transform the value inside a promise:
let (p, resolve) = Promise.pending();
p
->Promise.map(s => s ++ " world")
->Promise.get(s => Js.log(s));
resolve("Hello"); /* Hello world */
2310: syntax error, consider adding a `;' before
To be precise, Promise.map
creates a new promise with the transformed value.
If the function you are using to transform the value also returns a promise,
use Promise.flatMap
instead of Promise.map
. Promise.flatMap
will flatten the nested promise.
If you have a chain of promise operations, and you'd like to inspect the value
in the middle of the chain, use Promise.tap
:
let (p, resolve) = Promise.pending();
p
->Promise.tap(s => Js.log("Value is now: " ++ s))
->Promise.map(s => s ++ " world")
->Promise.tap(s => Js.log("Value is now: " ++ s))
->Promise.get(s => Js.log(s));
resolve("Hello");
/*
Value is now: Hello
Value is now: Hello world
Hello world
*/
2310: syntax error, consider adding a `;' before
Promise.race
waits for one of the promises passed to it to resolve:
[@bs.val]
external setTimeout: (unit => unit, int) => unit = "setTimeout";
let one_second = Promise.exec(resolve => setTimeout(resolve, 1000));
let five_seconds = Promise.exec(resolve => setTimeout(resolve, 5000));
Promise.race([one_second, five_seconds])
->Promise.get(() => { Js.log("Hello"); exit(0); });
/* Prints "Hello" after one second. */
2310: syntax error, consider adding a `;' before
Promise.all
instead waits for all of the promises passed to it,
concurrently:
[@bs.val]
external setTimeout: (unit => unit, int) => unit = "setTimeout";
let one_second = Promise.exec(resolve => setTimeout(resolve, 1000));
let five_seconds = Promise.exec(resolve => setTimeout(resolve, 5000));
Promise.all([one_second, five_seconds])
->Promise.get(_ => { Js.log("Hello"); exit(0); });
/* Prints "Hello" after five seconds. */
2310: syntax error, consider adding a `;' before
For convenience, there are several variants of Promise.all
:
Result
Promises that can fail are represented using the standard library's
Result
, and its constructors Ok
and Error
:
open Belt.Result;
Promise.resolved(Ok("Hello"))
->Promise.getOk(s => Js.log(s)); /* Hello */
2310: syntax error, consider adding a `;' before
Promise.getOk
waits for p
to have a value, and runs its function
only if that value is Ok(_)
. If you instead resolve the promise with
Error(_)
, there will be no output:
open Belt.Result;
Promise.resolved(Error("Failed"))
->Promise.getOk(s => Js.log(s)); /* Program just exits. */
2310: syntax error, consider adding a `;' before
You can wait for either kind of value by calling Promise.getOk
and
Promise.getError
:
open Belt.Result;
let () = {
let p = Promise.resolved(Error("Failed"));
p->Promise.getOk(s => Js.log(s));
p->Promise.getError(s => Js.log("Error: " ++ s));
}; /* Error: Failed */
967: syntax error, consider adding a `;' before
...or respond to all outcomes using the ordinary Promise.get
:
open Belt.Result;
Promise.resolved(Error("Failed"))
->Promise.get(result =>
switch (result) {
| Ok(s) => Js.log(s);
| Error(s) => Js.log("Error: " ++ s);
}); /* Error: Failed */
2310: syntax error, consider adding a `;' before
The full set of functions for handling results is:
Promise.getOk
Promise.tapOk
Promise.mapOk
Promise.flatMapOk
Promise.getError
Promise.tapError
Promise.mapError
Promise.flatMapError
There are also similar functions for working with Option
:
In addition, there is also a set of variants of Promise.all
for results, which
propagate any Error(_)
as soon as it is received:
Promise.allOk
Promise.allOk2
Promise.allOk3
Promise.allOk4
Promise.allOk5
Promise.allOk6
Promise.allOkArray
If you'd like instead to fully wait for all the promises to resolve with either
Ok(_)
or Error(_)
, you can use the ordinary Promise.all
and its variants.
As you can see from Handling errors, Promise
doesn't use rejection
for errors — but JavaScript promises do. In order to support bindings to
JavaScript libraries, which often return promises that can be rejected,
Promise
provides the Promise.Js
helper module.
Promise.Js
works the same way as Promise
. It similarly has:
However, because Promise.Js
uses JS rejection for error handling rather than
Result
or Option
,
Result
and Option
.Promise.Js.catch
for handling rejection.Promise.Js.rejected
for creating an
already-rejected promise.Underneath, Promise
and Promise.Js
have the same implementation:
type Promise.t('a) = Promise.Js.t('a, never);
3647: <UNKNOWN SYNTAX ERROR>
That is, Promise
is really Promise.Js
that has no rejection type, and no
exposed helpers for rejection.
There are several helpers for converting between Promise
and Promise.Js
:
Promise.Js.catch
can also perform a conversion to Promise
, if
you simply convert a rejection to a resolution. In the next example, note the
final line is no longer using Promise.Js
, but Promise
:
Promise.Js.rejected("Failed")
->Promise.Js.catch(s => Promise.resolved("Error: " ++ s))
->Promise.get(s => Js.log(s)); /* Error: Failed */
2310: syntax error, consider adding a `;' before
There are also two functions for converting between Promise.Js
and the current
promise binding in the BuckleScript standard libarary, Js.Promise
:
Because both libraries are bindings for the same exact kind of value, these are both no-op identity functions that only change the type.
Refer to the example node-fetch binding repo.
When you want to bind a JS function that returns a promise, you can use
Promise
directly in its return value:
/* A mock JS library. */
[%%bs.raw {|
function delay(value, milliseconds) {
return new Promise(function(resolve) {
setTimeout(function() { resolve(value); }, milliseconds)
});
}|}]
/* Our binding. */
[@bs.val]
external delay: ('a, int) => Promise.t('a) = "delay";
/* Usage. */
delay("Hello", 1000)
->Promise.get(s => Js.log(s));
/* Prints "Hello" after one second. */
2305: syntax error, consider adding a `;' before
If the promise can be rejected, you should use Promise.Js
instead, and
convert to Promise
as quickly as possible, with intelligent
handling of rejection. Here is one way to do that:
/* Mock JS library. */
[%%bs.raw {|
function delayReject(value, milliseconds) {
return new Promise(function(resolve, reject) {
setTimeout(function() { reject(value); }, milliseconds)
});
}|}]
/* Binding. */
[@bs.val]
external delayRejectRaw: ('a, int) => Promise.Js.t(_, 'a) = "delayReject";
let delayReject = (value, milliseconds) =>
delayRejectRaw(value, milliseconds)
->Promise.Js.toResult;
/* Usage. */
delayReject("Hello", 1000)
->Promise.getError(s => Js.log(s));
/* Prints "Hello" after one second. */
2305: syntax error, consider adding a `;' before
Note that this binding has two steps: there is a raw binding, and then an extra
wrapper that converts rejections into Result
s. If the potential rejections
are messy, this is a good place to insert additional logic for converting them
to nice Reason values :)
When passing a promise to JS, it is generally safe to use Promise
rather
than Promise.Js
:
/* Mock JS library. */
[%%bs.raw {|
function log(p) {
p.then(function (v) { console.log(v); });
}|}]
/* Binding. */
[@bs.val]
external log: Promise.t('a) => unit = "log";
/* Usage. */
log(Promise.resolved("Hello")); /* Hello */
2305: syntax error, consider adding a `;' before
The JS function Promise.resolve
has a special case, which
is triggered when you try to resolve a promise with another, nested promise.
Unfortunately, this special case makes it impossible to assign
Promise.resolve
a consistent type in Reason (and most type systems).
Here are the details. The code will use
Js.Promise.resolve
, BuckleScript's direct binding to
JS's Promise.resolve
.
Js.Promise.resolve
takes a value, and creates a promise containing that value:
Js.log(Js.Promise.resolve(1));
/* Promise { 1 } */
Js.log(Js.Promise.resolve("foo"));
/* Promise { 'foo' } */
let _ = Js.log (Js.Promise.resolve 1)
let _ = Js.log (Js.Promise.resolve "foo")
So, we should give it the type
Js.Promise.resolve: 'a => Js.Promise.t('a);
2310: syntax error, consider adding a `;' before
and, indeed, that's the type it has in BuckleScript.
Following the pattern, we would expect:
let nestedPromise = Js.Promise.resolve(1);
Js.log(Js.Promise.resolve(nestedPromise));
/* Promise { Promise { 1 } } */
let nestedPromise = Js.Promise.resolve 1
let _ = Js.log (Js.Promise.resolve nestedPromise)
But that's not what happens! Instead, the output is just
/* Promise { 1 } */
The nested promise is missing! But the type system, following the pattern, still thinks that this resulting value has type
Js.Promise.t(Js.Promise.t(int))
let _ = Js.Promise.t (Js.Promise.t int)
i.e., the type of the value we were (reasonably) expecting.
When you pass nestedPromise
to Js.Promise.resolve
, JS unwraps
nestedPromise
, violating the type! There is no easy way to encode such special
casing in the type system — especially since JS does it not only to
nested promises, but to any would-be nested object that has a .then
method.
The result is, if your program executes something like this, it will have ordinary values in places where it expects another level of promises. For example, if you do
let nestedPromise = Js.Promise.resolve(1);
Js.Promise.resolve(nestedPromise)
->Js.Promise.then_(p => /* ... */)
2310: syntax error, consider adding a `;' before
you would expect p
in the callback to be a promise containing 1
, and the
type of p
is indeed Js.Promise.t(int)
. Instead, however, p
is just the
bare value 1
. That means the callback will cause a runtime error as soon as
it tries to use promise functions on the 1
. Worse, you might store p
in a
data structure, and the runtime error will occur at a very distant place in the
code. The type system is supposed to prevent such errors! That's part of the
point of using Reason.
The same special casing occurs throughout the JS Promise
API — for
example, when you return a promise from the callback of then_
. This means that
most of the JS Promise
functions can't be assigned a correct type and
directly, safely be used from Reason.
reason-promise
makes promises type-safeThe previous section shows that JS promise functions are broken. An important observation is that it is only the functions that are broken — the promise data structure is not. That means that to make JS promises type-safe, we can keep the existing JS data structure, and just provide safe replacement functions to use with it in Reason. This is good news for interop :)
To fix the functions, only the special-case flattening
has to be undone. So, when you call reason-promise
's
Promise.resolved(value)
, it checks whether value
is a promise
or not, and...
If value
is not a promise, reason-promise
just passes it to JS's
Promise.resolve
, because JS will do the right thing.
If value
is a promise, it's not safe to simply pass it to JS, because it
will trigger the special-casing. So, reason-promise
boxes the nested
promise:
let nestedPromise = Promise.resolved(1);
Js.log(Promise.resolved(nestedPromise));
/* Promise { PromiseBox { Promise { 1 } } } */
let nestedPromise = Promise.resolved 1
let _ = Js.log (Promise.resolved nestedPromise)
This box, of course, is not a promise, so inserting it in the middle is enough to suppress the special-casing.
Whenever you try to take the value out of this resulting structure (for
example, by calling Promise.get
on it), reason-promise
transparently unboxes the PromiseBox
and passes the nested promise to your
callback — as your callback would expect.
This conditional boxing and unboxing is done throughout reason-promise
. It
only happens for nested promises, and anything else with a .then
method. For
all other values, reason-promise
behaves, internally, exactly like JS
Promise
(though with a cleaner outer API). This is enough to make promises
type-safe.
This is a simple scheme, but reason-promise
includes a very thorough
test suite to be extra sure that it always manages the boxing
correctly.
This conditional boxing is similar to how unboxed optionals are implemented in BuckleScript. Optionals are almost always unboxed, but when BuckleScript isn't sure that the unboxing will be safe, it inserts a runtime check that boxes some values, while still keeping most values unboxed.