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 }); } }