Hi! I'm Luke

@luke_dot_js

@lukewestby

humblespark.com

If you're reading .then(), it's too late

Handling effects in JavaScript

@luke_dot_js

@lukewestby

Effect:

Any interaction with the outside world

@luke_dot_js

@lukewestby

const request = (url) => {
  return fetch(url).then((r) => r.json());
};

const getUser = (id) => {
  return db("users").where({ id }).first()
    .then((user) => user.id);
};

const currentOffset = (el) => {
  return window.scrollY - el.scrollTop;
};

@luke_dot_js

@lukewestby

@luke_dot_js

@lukewestby

@luke_dot_js

@lukewestby

What is the right way to deal with effects?

@luke_dot_js

@lukewestby

doSomeWork((error, result) => {
  if (error) respond(error, 500);
  else respond(result, 200);
});

Callbacks

doSomeWork((error, firstResult) => {
  if (error) return respond(error, 500);
  else {
    processFirstResult(firstResult, (secondError, secondResult) => {
    if (secondError) return respond(secondError, 500);
    else {
      if (secondResult.prop) {
        doFirstBranchWork(secondResult, (firstBranchError, firstBranchResult) => {
          if (firstBranchError) respond(firstBranchError, 500);
          else respond(firstBranchResult, 200);
        });
      } else {
        doSecondBranchWork(secondResult, (secondBranchError, secondBranchResult) => {
          if (secondBranchError) respond(secondBranchError, 500);
          else respond(secondBranchError);
        });
      }
    }
  });
});

@luke_dot_js

@lukewestby

doSomeWork()
  .then((result) => processFirstResult(result))
  .then((result) => {
    return result.prop ?
      doFirstBranchWork(result) :
      doSecondBranchWork(result);
  })
  .then((result) => respond(result, 200))
  .catch((error) => respond(error, 500));

Promises

@luke_dot_js

@lukewestby

@luke_dot_js

@lukewestby

🎉

@luke_dot_js

@lukewestby

😢

const myPromiseFn = (arg) => {
    return new Promise((resolve, reject) => {
        doWork(arg, (ev) => {
            if (ev.error) {
                reject(ev.error);
            } else {
                resolve(ev.data);
            }
        });
    });
});

callbacks

executor

rejection

resolution

@luke_dot_js

@lukewestby

From MDN:

@luke_dot_js

@lukewestby

If we’re doing async work, effects are (almost) always involved

Promises represent the creation of async work

If a Promise is present, we can pretty safely assume the present of a side-effect

@luke_dot_js

@lukewestby

side-effects

@luke_dot_js

@lukewestby

😱😱😱😱😱😱😱😱😱

This is super easy to understand:

This is less easy to understand:

(user) => user.id
db("users").first()
  .then((user) => user.id)

@luke_dot_js

@lukewestby

@luke_dot_js

@lukewestby

elm-lang.org

@luke_dot_js

@lukewestby

vs.

const getUser = (id) => {
  return db("users")
    .where({ id })
    .first();
};
const getUser = (id) => ({
  action: "db",
  table: "users",
  operations: [ ["where", { id }], ["first"] ]
});

@luke_dot_js

@lukewestby

Somewhere else...

const dbRunner = ({ table, operations }) => {
  const query = db(table);
  return operations.reduce(/* ... */, query);
};

@luke_dot_js

@lukewestby

Declare effects in application code

Pause execution of the application code to run the effect

Resume application code when the effect has produced a result

@luke_dot_js

@lukewestby

React

type ReactDOMElement = {
  type: string,
  props: {
    children: ReactNodeList,
    className: string,
    ...
  },
  key: string | boolean | number | null,
  ref: string | null
};

@luke_dot_js

@lukewestby

Cycle.js

@luke_dot_js

@lukewestby

const actionHandlers = {
  [Actions.begin](request) {
    return Actions.dbCall({
      table: "users",
      operations: [
        ["where", { id: request.params.id }],
        ["first"],
      ]
    });
  },
  [Actions.dbCallSuccess](request, user) {
    return Actions.respond(user.id, 200);
  },
  [Actions.dbCallFailure](request, error) {
    return Actions.respond(error, 500);
  }
};
server.route({
  method: "GET",
  path: "/users/{id}",
  handler(request, reply) {
    runActions(runners, actionHandlers, request)
      .then((result) => {
        reply(result.data).statusCode(result.status);
      })
      .catch((error) => {
        reply(error.message).statusCode(error.status);
      });
  }
});

@luke_dot_js

@lukewestby

const actionHandlers = {
  [Actions.begin](request) {
    return Actions.dbCall({
      table: "users",
      operations: [
        ["where", { id: request.params.id }],
        ["first"],
      ]
    });
  },
  [Actions.dbCallSuccess](request, user) {
    return Actions.respond(user.id, 200);
  },
  [Actions.dbCallFailure](request, error) {
    return Actions.respond(error, 500);
  }
};
const actionHandlers = {
  [0](request) {
    return dbCall({
      table: "users",
      operations: [
        ["where", { id: request.params.id }],
        ["first"],
      ]
    });
  },
  [1](request, user) {
    return respond(user.id, 200);
  },
  error(request, error) {
    return respond(error, 500);
  }
};
server.route({
  method: "GET",
  path: "/users/{id}",
  handler(request, reply) {
    runActions(runners, handler(request))
      .then((result) => {
        reply(result.data)
          .statusCode(result.status);
      })
      .catch((error) => {
        reply(error.message)
          .statusCode(error.status);
      });
  }
});

@luke_dot_js

@lukewestby

Generators!

const handler = function* (request) {
  try {
    const user = yield {
      type: "db",
      table: "users",
      operations: [/* ... */]
    };

    return respond(user.id, 200);
  } catch (error) {
    respond(error.message, 500);
  }
};
const iterator = handler({ params: { id: 1 } });

let next = iterator.next();
assertDeepEqual(next.value, { type: "db", /* ... */ });

next = iterator.next({ id: 1, name: "Luke", /* ... */ });
assertDeepEqual(next.value, respond(1, 200));

// or

next = iterator.throw(Error("NOPE!");
assertDeepEqual(next.value, respond("NOPE!", 500));

@luke_dot_js

@lukewestby

@luke_dot_js

@lukewestby

🎉

@luke_dot_js

@lukewestby

😢

Two problems:

  1. Nested calls are uncomfortable
  2. Parallelization isn’t a given

@luke_dot_js

@lukewestby

const handler = function* (request) {
  try {
    const user = yield {
      type: “db",
      table: “users",
      operations: [/* ... */]
    };

    return respond(user.id, 200);
  } catch (error) {
    respond(error.message, 500);
  }
};

@luke_dot_js

@lukewestby

const handler = function* (request) {
  try {
    const userId = ?? getUserId(request); ??
  } catch (error) {
    respond(error.message, 500);
  }
};

@luke_dot_js

@lukewestby

const handler = function* (request) {
  try {
    const userId = yield spawn(getUserId, request);
  } catch (error) {
    respond(error.message, 500);
  }
};

@luke_dot_js

@lukewestby

Only one level of continuation is possible with Generators

What if we could yield deeply?

@luke_dot_js

@lukewestby

One-shot delimited continuations with effect handlers

Sebastian Markbåge

@luke_dot_js

@lukewestby

const funcWithEffect = (request) => {
  const user = perform { type: “db”, ... };
  return user.id;
}

try {
  const result = funcWithEffect(request);
} catch effect -> [{ type, ...details }, next] {  
  runners[type](details)
 .then((result) => next(result))
 .catch((error) => { throw error; });  
}

@luke_dot_js

@lukewestby

This isn't a part of JS

What can we do today?

@luke_dot_js

@lukewestby

monads

@luke_dot_js

@lukewestby

😱😱😱😱😱😱😱😱😱

Task!

Folktale.js - data.task

const Task = require('data.task');

// getUser :: Int -> Task Error User
const getUser = (id) => {
  return new Task((resolve, reject) => {
    db('table')
      .where({ id })
      .first()
      .then(resolve, reject);
  });
};
// userHandler :: Request -> Task Error Response
const userHandler = (request) => {
  return getUser(request.params.id)
    .map((user) => respond(user, 200))
    .rejectedMap((error) => respond(error, 500));
};

@luke_dot_js

@lukewestby

server.route({
  method: "GET",
  path: "/users/{id}",
  handler(request, reply) {
    userHandler(request).fork(reply, reply);
  }
});

Nothing runs until right here

@luke_dot_js

@lukewestby

Tradeoffs

  • Free of side-effects
  • Composable
  • Easy to parallelize
    • ​control.async
  • Not representable as pure data
  • Requires mocking

@luke_dot_js

@lukewestby

Luke's Very Official Recommendation™

On the backend:

Use Tasks

 

On the frontend:

Try Elm

Try Cycle.js

Use Tasks

Questions?

@luke_dot_js

@lukewestby

If You're Reading .then() It's Too Late

By lukewestby

If You're Reading .then() It's Too Late

  • 973