Async with ES6 Generators

Confession: I don't like writing about code. On a scale of being forced to watch musical theater to reincarnated David Bowie telling me I have cool style, writing about code is like a three. I also don't care much to read about it, but would be absolute shit at my job if I didn't. As such, when I come across something that improves my workflow, I feel mildly obligated to give back to the community from which I take so much. Hence, this post about using ES6 generator functions to handle asynchronous code in a synchronous fashion.

Say we want to make a two API calls, and the second is dependent on the response of the first. Instead of writing a jumbled mess of callback malarkey (even with Promises you have to nest and nest and nest), we can do something slick like this:

const request = (url) => fetch(url).then((response) => getStuff.next(response.json()));

function* getStuff() {
  const payload1 = yield request('/getData');
  const payload2 = yield request(`/getMoreData/${payload1.id}`);
  // etc
}

Here we make two asynchronous GET requests using fetch , calling .next() on the generator from inside of .then(). .next() not only un-pauses the generator but can also inject values back into it, in this case the returned promise response.json(). The second fetch won't fire until the first has resolved, therefore we can treat payload1 like a regular old value, and use it in the argument of our next request.

redux-saga

With redux-saga , you don't have to worry about calling .next() on your generators because magic (middleware) takes care of that for you. Our code ends up looking more like this:

import { call } from 'redux-saga/effects';
import { takeLatest } from 'redux-saga';
import * as actions from './actions';

const request = (url) => fetch(url).then((response) => response.json());

function* getStuff() {
  const payload1 = yield call(request, '/getData');
  const payload2 = yield call(request, `getMoreData/${payload1.id}`);
  yield put({ type: actions.GET_STUFF_SUCCEEEDED, payload2 });
}
  
function* getStuffSaga() {
  yield* takeLatest(actions.GET_STUFF, getStuff)
}

Now the request function, or any of the code we write, is not responsible for iterating the generator. The takeLatest in getStuffSaga listens for dispatched GET_STUFF actions (a simple Redux object declared elsewhere) and runs the getStuff generator.

Yes, it's a lot more code than the first example and there is a learning curve, so this solution is not right for every situation. However, if you're juggling a number of generators and async operations, the saga pattern is a lovely thing. This tutorial will help you get started.

Error Handling

Because code inside generators executes synchronously, regardless of the fact that it may handle asynchronous external processes, you can use the try/catch pattern to handle errors. What's more is that you can shove multiple asynchronous operations into one try/catch block. The getStuff generator function in the code above can be rewritten to handle errors like this:

export function* getStuff() {
  try {
    const payload1 = yield call(request, '/getData');
    const payload2 = yield call(request, `/getData/${payload1.id}`);
    yield put({ type: action.GET_STUFF_SUCCEEEDED, payload2 });
  } catch (error) {
    yield put({ type: actions.GET_STUFF_FAILED, error });
  }
}
Tags ,