redux-saga: uncaught error from Promise.reject

A demo here.

Code related below, the race code mostly comes from https://github.com/yelouafi/redux-saga/issues/183:

import { fork, call, put, race, take } from 'redux-saga/effects'
function * api () {
  try {
    let result = yield * callWithLoadingEffect(fetchApi)
    console.log(result)
  } catch (error) {
    console.log('error', error)
  }
}
// fetch data from reddit
function fetchApi () {
  return fetch('https://www.reddit.com/new.json')
    .then(
      (response) => {
        return Promise.reject('haha')
        // return response.json()
      }
    )
    .catch(
      (error) => Promise.reject(error)
    )
}
const delay = (ms) => new Promise((resolve) => setTimeout(() => resolve(true), ms))

export function * callWithLoadingEffect (fn) {
  try {
    const task = yield fork(fn)
    const taskPromise = new Promise((resolve, reject) => {
      task.done.then(resolve, reject)
    })
    let {timeout, result} = yield race({
      timeout: call(delay, 100),
      result: taskPromise
    })
    if (timeout) {
      yield put({
        type: 'SHOW_LOADING'
      })
      result = yield taskPromise
    }
    yield put({
      type: 'HIDE_LOADING'
    })
    return result
  } catch (err) {
    yield put({
      type: 'HIDE_LOADING'
    })
    throw err
  }
}
export function * helloSaga () {
  console.log('Hello Sagas!')
  while (true) {
    yield take('FETCH')
    yield fork(api)
  }
}

If I returned Promise.reject('haha') when response comes back, proc.js would print errors:

screen shot 2016-04-10 at 18 28 27

I thought the try {} catch () {} in callWithLoadingEffect would catch the error as doc said, and from the console, I can see api generator did catch the error, but that uncaught haha seems like that it isn’t true, any suggest?

Thanks.

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Reactions: 1
  • Comments: 16 (16 by maintainers)

Most upvoted comments

I think you can get the same effect and a simpler code with try/finally

export function * callWithLoadingEffect (fn) {
  if (typeof fn !== 'function') {
    throw new Error('callWithLoadingEffect only accept function as its argument')
  }
  try {
    const task = yield fork(fn)
    let {timeout, result} = yield race({
      timeout: call(delay, 20),
      result: join(task)
    })
    if (timeout) {
      yield put(loadingShow())
      result = yield join(task)
    }
    return result
  } finally {
    yield put(loadingHide())
  }
}

IMO, you shouldn’t use any try/catch with a join effect. The main purpose of a join is to wait for a task to terminate. The task should handle all its errors and let only bubble unexpected ones (see below)

I’d rather separate errors in 2 classes

  • Business errors are expected errors and should be handled inside the forked task itself.
  • Bugs are unexpected errors and will simply bubble up the fork chain up to the root. All sagas in the current execution tree will be cancelled and the error will bubble up. I can’t see how a program can recover from a bug. The only options I think of is to show some message to the user that the app has encountered an unexpected error, send some report then close the app to avoid any inconsistent state.

At least the best way to work with the redux-saga fork model is to handle business errors from inside the forked tasks themselves an let bubble only bugs which can be caught at some higher level.

In fact I can even go farther and recommend to not use try/catch for business error handling because javascript IMO lacks the necessary constructs for typed catch blocks. consider this code

function* myCallabletask() {
  try {
    yield call(Api.gett) // note typo
    yield put(RequestSuccess)
  } catch(err) {
    yield put(RequestError) // will put on any kind of error including Runtime errors/bugs
  }
}

because the catch block catches all errors: both business Errors (server error, timeout …) and bugs (Api.gett is not function). The code reacts the same on both types; I don’t think this the desired behavior. Typically we want to put an Error action only on server errors and let bubble any bug. In typed languages like Java you can achieve this with typed catch blocks likecatch (ServerError err) But JS lacks this feature.

The best way to think of the fork model is as dynamic parallel effect. A saga has a main flow (its own body => the main task) and can fork parallel flows dynamically. Like in parallel effects, cancelling the saga will cancel all the parallel tasks (main task + forked tasks). The saga will terminate after all parallel tasks terminate, and an error from one of the tasks will abort the whole parallel effects.

It may sound restrictive but this has the advantage of having a much simpler model to reason about. You know precisely how do Return values, Errors and Cancellations propagate, … The other option is the very flexible Promise model but also its well known issues (esp. Error propagation => unhandled rejections, error swallowed, not to mention the difficulty to define a simple and consistent model for Cancellations)