Skip to content

Instantly share code, notes, and snippets.

@lilactown
Last active August 20, 2022 07:56
Show Gist options
  • Star 81 You must be signed in to star a gist
  • Fork 10 You must be signed in to fork a gist
  • Save lilactown/a8d1dc6aa2043efa62b23e559291053e to your computer and use it in GitHub Desktop.
Save lilactown/a8d1dc6aa2043efa62b23e559291053e to your computer and use it in GitHub Desktop.
Notes on using JavaScript Promises in ReasonML/BuckleScript
/**
* Making promises
*/
let okPromise = Js.Promise.make((~resolve, ~reject as _) => [@bs] resolve("ok"));
/* Simpler promise creation for static values */
Js.Promise.resolve("easy");
Js.Promise.reject(Invalid_argument("too easy"));
/* Create a promise that resolves much later */
let timer =
Js.Promise.make(
(~resolve, ~reject as _) => {
ignore(Js.Global.setTimeout(() => [@bs] resolve("Done!"), 1000));
()
}
);
/**
* Handling promise values
* Note that we *have* to return a new promise inside of the callback given to then_;
*/
Js.Promise.then_((value) => Js.Promise.resolve(Js.log(value)), okPromise);
/* Chaining */
Js.Promise.then_(
(value) => Js.Promise.resolve(Js.log(value)),
Js.Promise.then_((value) => Js.Promise.resolve(value + 1), Js.Promise.resolve(1))
);
/* Better with pipes 😉 */
Js.Promise.resolve(1)
|> Js.Promise.then_((value) => Js.Promise.resolve(value + 1))
|> Js.Promise.then_((value) => Js.Promise.resolve(Js.log(value)));
/* And even better with some Reason spice */
Js.Promise.(
resolve(1)
|> then_((value) => resolve(value + 1))
|> then_((value) => resolve(Js.log(value)))
);
/* Waiting for two values */
Js.Promise.(
all2((resolve(1), resolve("a")))
|> then_(
((v1, v2)) => {
Js.log("Value 1: " ++ string_of_int(v1));
Js.log("Value 2: " ++ v2);
resolve()
}
)
);
/* Waiting for an array of values */
Js.Promise.(
all([|resolve(1), resolve(2), resolve(3)|])
|> then_(
([|v1, v2, v3|]) => {
Js.log("Value 1: " ++ string_of_int(v1));
Js.log("Value 2: " ++ string_of_int(v2));
Js.log("Value 3: " ++ string_of_int(v3));
resolve()
}
)
);
/**
* Error handling
*/
/* Using a built-in OCaml error */
let notFoundPromise = Js.Promise.make((~resolve as _, ~reject) => [@bs] reject(Not_found));
Js.Promise.then_((value) => Js.Promise.resolve(Js.log(value)), notFoundPromise)
|> Js.Promise.catch((err) => Js.Promise.resolve(Js.log(err)));
/* Using a custom error */
exception Oh_no(string);
let ohNoPromise = Js.Promise.make((~resolve as _, ~reject) => [@bs] reject(Oh_no("oh no")));
Js.Promise.catch((err) => Js.Promise.resolve(Js.log(err)), ohNoPromise);
/**
* Unfortunately, as one can see - catch expects a very generic `Js.Promise.error` value
* that doesn't give us much to do with.
* In general, we should not rely on rejecting/catching errors for control flow;
* it's much better to use a `result` type instead.
*/
let betterOk =
Js.Promise.make((~resolve, ~reject as _) => [@bs] resolve(Js.Result.Ok("everything's fine")));
let betterOhNo =
Js.Promise.make((~resolve, ~reject as _) => [@bs] resolve(Js.Result.Error("nope nope nope")));
let handleResult =
Js.Promise.then_(
(result) =>
Js.Promise.resolve(
switch result {
| Js.Result.Ok(text) => Js.log("OK: " ++ text)
| Js.Result.Error(reason) => Js.log("Oh no: " ++ reason)
}
)
);
handleResult(betterOk);
handleResult(betterOhNo);
/**
* "Better living through functions."
* This section is for collecting useful helper functions when handling promises
*/
/* Get rid of the need for returning a promise every time we use `then_` */
let thenResolve = (fn) => Js.Promise.then_((value) => Js.Promise.resolve(fn(value)));
Js.Promise.(resolve(1) |> thenResolve((value) => value + 1) |> thenResolve(Js.log));
/* Get rid of pesky compiler warnings at the end of a side-effectful promise chain */
let thenIgnore = (fn, p) => thenResolve((value) => fn(value), p) |> ignore;
@peerreynders
Copy link

peerreynders commented Feb 11, 2018

 * it's much better to use a `result` type instead.

Actually, it gets annoying really quickly that the error has entered the "resolving" chain if you need to keep chaining a returned promise. It should be preferred that:

  • The final consumer of the promise is responsible for explicitly using Js.Promise.catch to ensure that the promise rejection isn't "unhandled". It's only at this point in time that it makes sense to resolve from a rejecting chain in order to match the type of the resolving chain (if the final rejection handler returns rejected promise instead of a resolved one, an unhandledrejection event is fired (on Node.js: unhandledRejection)).
  • Intermediate stages may use Js.Promise.catch to enhance or modify the error information but must continue the rejecting chain (or raise an exception which will continue the rejecting chain).
  • While it's possible at any point in time to transfer from the resolving chain to the rejecting chain (as the result of an error), transfer in the the opposite direction should be discouraged (until the absolute final consumer of the promise) - otherwise further chaining will require additional checks in the resolving chain that will likely push errors from previous stages back into the rejecting chain.

.

  /* Demo: rejected stays rejected until the very last stage */

  exception ExnDemo(string);

  let onLoad = () => {

    let addErrorInfo = (error) => {
      /* https://bucklescript.github.io/docs/en/interop-misc.html#safe-external-data-handling */
      let enhanceError = [@bs.open]
        fun
        | ExnDemo(message) => ExnDemo(message ++ ", I don't believe it");

      switch (enhanceError(error)) {
      | Some(exn) => exn
      | None      => ExnDemo("Unknown Exception")
      }
      |> Js.Promise.reject
    };

    let logErrorMessage = (error) => {
      let exnMessage = [@bs.open]
        fun
        | ExnDemo(message) => message;

      switch (exnMessage(error)) {
      | Some(message) => message
      | None => "Unidentified Error"
      }
      |> Js.log
    };

    let resolved =
      Js.Promise.make((~resolve, ~reject as _) => [@bs] resolve("everything's fine"));

    let rejected =
      Js.Promise.make((~resolve as _, ~reject) => [@bs] reject(ExnDemo("nope nope nope")));

    let handleResult = (promise) => {
      Js.Promise.(
        then_((text) => text ++ " and dandy" |> resolve, promise) /* then_ processes resolved result in the resolving chain. */
        |> catch((error) => addErrorInfo(error))                  /* Intermediate catch adds error information in the rejecting chain. */
        |> then_((text) => Js.log(text) |> resolve)               /* Final then_ for result in the resolving chain. */
        |> catch((error) => logErrorMessage(error) |> resolve)    /* Final catch to deal with ANY error in the rejecting chain. */
      );                                                          /* The catch only resolves here to prevent an "unhandledrejection" event. */
    };                                                            /* The type of the resolved promise from the rejection handler must */
                                                                  /* match that of the resolving chain - in this case Js.Promise.t(unit). */
    handleResult(resolved) |> ignore;
    handleResult(rejected) |> ignore;
  };

Alternately a case could be made for ignoring Js.Promise.reject altogether as it only accepts type exn. Given that the promise is a JavaScript construct it may make more sense to instead consistently reject promises with Js.Exn.raiseError and the like (i.e. JavaScript Errors).

Example:

  let onLoad = () => {

    let jsErrorToExn = jsError => {
      let isObject = rawObj =>
        Js.typeof(rawObj) == "object"
        && ! Js.Array.isArray(rawObj)
        && ! ((Obj.magic(rawObj): Js.null('a)) === Js.null);

      let couldBeError = rawObj => {
        let obj: {
          .
          "name": string,
          "message": string
        } =
          Obj.magic(rawObj);

        ! ((Obj.magic(obj##message): Js.undefined('a)) === Js.undefined)
        && ! ((Obj.magic(obj##name): Js.undefined('a)) === Js.undefined)
        && Js.String.includes("Error", obj##name);
      };
      isObject(jsError) ?
        couldBeError(jsError) ?
          Some(Js.Exn.internalToOCamlException(Obj.magic(jsError): Obj.t)) : None :
        None;
    };

    let errorMessage = (~default="Unknown Error", jsError) =>
      switch (jsErrorToExn(jsError)) {
      | Some(Js.Exn.Error(e)) =>
        switch (Js.Exn.message(e)) {
        | None => default
        | Some(message) => message
        }
      | _ => default
      };

    let logError = jsError => {
      let log = (e, (prefix, f)) =>
        switch (f(e)) {
        | None => ()
        | Some(text) => Js.log2(prefix, text)
        };
      switch (jsErrorToExn(jsError)) {
      | Some(Js.Exn.Error(e)) =>
        List.iter(
          log(e),
          [
            ("name: ", Js.Exn.name),
            ("message: ", Js.Exn.message),
            ("fileName: ", Js.Exn.fileName),
            ("stack: ", Js.Exn.stack)
          ]
        )
      | _ => Js.log("Unidentified Error")
      };
    };

    let addErrorInfo = error =>
      errorMessage(error) ++ ", I don't believe it" |> Js.Exn.raiseError; /* throws Error to reject */

    let resolved =
      Js.Promise.make((~resolve, ~reject as _) =>
        [@bs] resolve("everything's fine")
      );

    let rejected =
      Js.Promise.make((~resolve as _, ~reject as _) =>
        Js.Exn.raiseError("nope nope nope")
      ); /* throws Error to reject */

    let handleResult = promise =>
      Js.Promise.(
        then_(text => text ++ " and dandy" |> resolve, promise)
        |> catch(error => addErrorInfo(error))
        |> then_(text => Js.log(text) |> resolve)
        |> catch(error => logError(error) |> resolve)
      );

    handleResult(resolved) |> ignore;
    handleResult(rejected) |> ignore;
  };

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment