UNPKG

@ryyppy/rescript-promise

Version:

2020 proposal for new ReScript promise bindings

402 lines (311 loc) 9.61 kB
# rescript-promise This is a proposal for replacing the original `Js.Promise` binding that is shipped within the ReScript compiler. It will be upstreamed as `Js.Promise2` soon. This binding was made to allow our users to try out the implementation in their codebases first. > See the [PROPOSAL.md](./PROPOSAL.md) for the rationale and design decisions. **Feature Overview:** - `t-first` bindings - Fully compatible with the builtin `Js.Promise.t` type - `make` for creating a new promise with a `(resolve, reject) => {}` callback - `resolve` for creating a resolved promise - `reject` for creating a rejected promise - `catch` for catching any JS or ReScript errors (all represented as an `exn` value) - `then` for chaining functions that return another promise - `thenResolve` for chaining functions that transform the value inside a promise - `all` and `race` for running promises concurrently - `finally` for arbitrary tasks after a promise has rejected / resolved - Globally accessible `Promise` module that doesn't collide with `Js.Promise` **Non-Goals of `rescript-promise`:** - No rejection tracking or other complex type hackery - No special utilities (we will add docs on how to implement common utils on your own) **Caveats:** - There are 2 edge-cases where returning a `Promise.t<Promise.t<'a>>` value within `then` / `thenResolve` is not runtime safe (but also quite rare in practise). Refer to the [Common Mistakes](#common-mistakes) section for details. - These edge-cases shouldn't happen in day to day use, also, for those with general concerns about runtime safetiness, it is recommended to use a `catch` call in the end of each promise chain to prevent runtime crashes anyways (just like in JS). ## Requirements `bs-platform@8.2` and above. ## Installation ``` npm install @ryyppy/rescript-promise --save ``` Add `@ryyppy/rescript-promise` as a dependency in your `bsconfig.json`: ```json { "bs-dependencies": ["@ryyppy/rescript-promise"] } ``` This will expose a global `Promise` module (don't worry, it will not mess with your existing `Js.Promise` code). ## Examples - [examples/FetchExample.res](examples/FetchExample.res): Using the `fetch` api to login / query some data with a full promise chain scenario ## Usage **Creating a Promise:** ```rescript let p1 = Promise.make((resolve, _reject) => { // We use uncurried functions for resolve / reject // for cleaner JS output without unintended curry calls resolve(. "hello world") }) let p2 = Promise.resolve("some value") // You can only reject `exn` values for streamlined catch handling exception MyOwnError(string) let p3 = Promise.reject(MyOwnError("some rejection")) ``` **Access and transform a promise value:** ```rescript open Promise Promise.resolve("hello world") ->then(msg => { // then callbacks require the result to be resolved explicitly resolve("Message: " ++ msg) }) ->then(msg => { Js.log(msg); // Even if there is no result, we need to use resolve() to return a promise resolve() }) ->ignore // Requires ignoring due to unhandled return value ``` **Chain promises:** ```rescript open Promise type user = {"name": string} type comment = string // mock function let queryComments = (username: string): Js.Promise.t<array<comment>> => { switch username { | "patrick" => ["comment 1", "comment 2"] | _ => [] }->resolve } // mock function let queryUser = (_: string): Js.Promise.t<user> => { resolve({"name": "patrick"}) } let queryUser = queryUser("u1") ->then(user => { // We use `then` to automatically // unnest our queryComments promise queryComments(user["name"]) }) ->then(comments => { // comments is now an array<comment> Belt.Array.forEach(comments, comment => Js.log(comment)) // Output: // comment 1 // comment 2 resolve() }) ->ignore ``` You can also use `thenResolve` to chain a promise, and transform its nested value: ```rescript open Promise let createNumPromise = (n) => resolve(n) createNumPromise(5) ->thenResolve(num => { num + 1 }) ->thenResolve(num => { Js.log(num) }) ->ignore ``` **Catch promise errors:** **Important:** `catch` needs to return the same return value as its previous `then` call (e.g. if you pass a `promise` of type `Promise.t<int>`, you need to return an `int` in your `catch` callback). This usually implies that you'll need to use a `result` value to express successful / unsuccessful operations: ```rescript exception MyError(string) open Promise Promise.reject(MyError("test")) ->then(str => { Js.log("this should not be reached: " ++ str) // Here we use the builtin `result` constructor `Ok` Ok("successful")->resolve }) ->catch(e => { let err = switch e { | MyError(str) => "found MyError: " ++ str | _ => "Some unknown error" } // Here we are using the same type (`t<result>`) as in the previous `then` call Error(err)->resolve }) ->then(result => { let msg = switch result { | Ok(str) => "Successful: " ++ str | Error(msg) => "Error: " ++ msg } Js.log(msg) resolve() }) ->ignore ``` **Catch promise errors caused by a thrown JS exception:** ```rescript open Promise let causeErr = () => { Js.Exn.raiseError("Some JS error")->resolve } Promise.resolve() ->then(_ => { causeErr() }) ->catch(e => { switch e { | JsError(obj) => switch Js.Exn.message(obj) { | Some(msg) => Js.log("Some JS error msg: " ++ msg) | None => Js.log("Must be some non-error value") } | _ => Js.log("Some unknown error") } resolve() // Outputs: Some JS error msg: Some JS error }) ->ignore ``` **Catch promise errors that can be caused by ReScript OR JS Errors (mixed error types):** Every value passed to `catch` are unified into an `exn` value, no matter if those errors were thrown in JS, or in ReScript. This is similar to how we [handle mixed JS / ReScript errors](https://rescript-lang.org/docs/manual/latest/exception#catch-both-rescript-and-js-exceptions-in-the-same-catch-clause) in synchronous try / catch blocks. ```rescript exception TestError(string) let causeJsErr = () => { Js.Exn.raiseError("Some JS error") } let causeReScriptErr = () => { raise(TestError("Some ReScript error")) } // imaginary randomizer function @bs.val external generateRandomInt: unit => int = "generateRandomInt" open Promise resolve() ->then(_ => { // We simulate a promise that either throws // a ReScript error, or JS error if generateRandomInt() > 5 { causeReScriptErr() } else { causeJsErr() }->resolve }) ->catch(e => { switch e { | TestError(msg) => Js.log("ReScript Error caught:" ++ msg) | JsError(obj) => switch Js.Exn.message(obj) { | Some(msg) => Js.log("Some JS error msg: " ++ msg) | None => Js.log("Must be some non-error value") } | _ => Js.log("Some unknown error") } resolve() }) ->ignore ``` **Using a promise from JS (interop):** ```rescript open Promise @val external someAsyncApi: unit => Js.Promise.t<string> = "someAsyncApi" someAsyncApi()->Promise.then((str) => Js.log(str)->resolve)->ignore ``` **Running multiple Promises concurrently:** ```rescript open Promise let place = ref(0) let delayedMsg = (ms, msg) => { Promise.make((resolve, _) => { Js.Global.setTimeout(() => { place := place.contents + 1 resolve(.(place.contents, msg)) }, ms)->ignore }) } let p1 = delayedMsg(1000, "is Anna") let p2 = delayedMsg(500, "myName") let p3 = delayedMsg(100, "Hi") all([p1, p2, p3])->then(arr => { // arr = [ [ 3, 'is Anna' ], [ 2, 'myName' ], [ 1, 'Hi' ] ] Belt.Array.forEach(arr, ((place, name)) => { Js.log(`Place ${Belt.Int.toString(place)} => ${name}`) }) // forEach output: // Place 3 => is Anna // Place 2 => myName // Place 1 => Hi resolve() }) ->ignore ``` **Race Promises:** ```rescript open Promise let racer = (ms, name) => { Promise.make((resolve, _) => { Js.Global.setTimeout(() => { resolve(. name) }, ms)->ignore }) } let promises = [racer(1000, "Turtle"), racer(500, "Hare"), racer(100, "Eagle")] race(promises) ->then(winner => { Js.log("Congrats: " ++ winner)->resolve // Congrats: Eagle }) ->ignore ``` ## Common Mistakes **Don't return a `Promise.t<Promise.t<'a>>` within a `then` callback:** ```rescript open Promise resolve(1) ->then((value: int) => { let someOtherPromise = resolve(value + 2) // BAD: this will cause a Promise.t<Promise.t<'a>> resolve(someOtherPromise) }) ->then((p: Promise.t<int>) => { // p is marked as a Promise, but it's actually an int // so this code will fail p->then((n) => Js.log(n)->resolve) }) ->catch((e) => { Js.log("luckily, our mistake will be caught here"); Js.log(e) // p.then is not a function resolve() }) ->ignore ``` **Don't return a `Promise.t<'a>` within a `thenResolve` callback:** ```rescript open Promise resolve(1) ->thenResolve((value: int) => { // BAD: This will cause a Promise.t<Promise.t<'a>> resolve(value) }) ->thenResolve((p: Promise.t<int>) => { // p is marked as a Promise, but it's actually an int // so this code will fail p->thenResolve((n) => Js.log(n))->ignore }) ->catch((e) => { Js.log("luckily, our mistake will be caught here"); // e: p.then is not a function resolve() }) ->ignore ``` ## Development ``` # Building npm run build # Watching npm run dev ``` ## Run Test Runs all tests ``` node tests/PromiseTest.js ``` ## Run Examples Examples are runnable on node, and require an active internet connection to be able to access external mockup apis. ``` node examples/FetchExample.js ```