A true story of taming promise chains with purescript

I had a big promise chain that ran a bunch of checks on an ethereum smart contract in sequence, in javascript, and it was kind of a pain. This code would happily do whatever if a return was omitted, a message substituted for a needed parameter, or just literally anything. And it’s a lot of code (611 lines of 1-line promise after 1-line promise). I needed to fix it one way or another.

Since I’ve experimented a lot with purescript I was curious whether I could clean it up and also improve my understanding of Aff in purescript. I think I understand it reasonably now.

Here is my short notelist about turning an async promise chain into a purescript fiber via Aff.

  1. purescript-aff-promise exists but I don’t understand things well enough to use it.
  2. fromEffFnAff is a function that takes a function of exactly 1 parameter and gives a 1-parameter Aff. I was frustrated for a pretty long time wondering what I was doing wrong passing it a function of more than one argument.
  3. The function fromEffFnAff expects to receive is a function that takes exactly two arguments, errFn and resolveFn. You may have to wrap your fancy javascript promise objects to make this work well, just in case.
  4. The right way to handle exceptions on an Aff chain is with purescript-transformers’ throwError (which consumes Error from Control.Monad.Eff.Exception when paired with the older Eff infrastructure), and catchError optionally (actually you can just let Error spill out and terminate the program, if you want the normal stack trace presentation).

What do notation gives you in this context is a running tally of what bindings are in scope through the promise chain, as well as the ability to write the chain in imperative style:

// Test.js
var q = require('q');
// ...
exports.callImpl = function(cfg) {
return function(err,response) {
// for example
q.delay(100).then(function() {
return cfg.message;
// Test.purs
type Message = { message :: String }
foreign import callImpl :: forall eff. Config -> EffFnAff (eff) String
callF :: forall eff. Config -> Aff (eff) String
callF = fromEffFnAff <<< callImpl

And you can use it like:

testbody :: forall eff. Unit -> Aff (console :: CONSOLE | eff) Unit
testbody _ = do
echo1 <- callF { message: "test1" }
echo2 <- callF { message: "test2" }
when (echo1 <> echo2 /= "test1test2") $ throwError $ error "not the right results from test"
log "Tests successful"

Bindings maintained, imperative code invisibly chopped into a promise chain.

Hopefully this very terse crib note version will be helpful. I went down a lot of blind alleys and had a well typed but non-working version at one point that I believe was creating and discarding Fiber objects.

Like what you read? Give art yerkes a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.