Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

'yield all' in Saga is not waiting for all the effects to complete

I have a Saga like so (some pseudocodish).

Saga1 calls an API. Based on the result, I need to call two further APIs. If all the APIs succeed, I call onSuccess, else onFailure.

The code seems to almost work fine, but not quite. The problem I facing with yield all is that it considered saga2 and saga3 complete as soon as the first yield put was called (see comment in saga2/3). It didn't wait for the fetch yield to finish.

I think it's partly due to my misunderstanding of what a "complete effect" means. But apart from that, I want yield all to wait until everything is done. I want any exceptions thrown by fetch in saga2/3 to be caught by catch in saga1.

saga1(action) {

    const { onSuccess, onFailure } = action.payload;

    try {
        yield fetch...

        if(response.some_condition) yield all([
            put(saga2()),
            put(saga3())
        ])

        onSuccess();

    }
    catch(e) {
        onFailure();
    }
}

saga2(action) {

    yield put(someaction()) // This yields
    yield fetch...
}

saga3(action) {

    yield put(someaction()) // This yield
    yield fetch...
}

This code below is related to my comment below about catch not working

action1 () { // action2 is same
    try {
        yield fetch();
        yield put(FINISHED_1);
    }
    catch(e) {
        throw (e);
    }
}

saga1() {
    try {
        yield put(action1());
        yield put(action2());

        yield all([
            take(FINISHED_1),
            take(FINISHED_2),
        ])
        console.log("this doesn't print if exception in either action");
    }
    catch(e) {
        console.log("this doesn't print if exception in either action");
    }
    finally {
        console.log("this prints fine");
    }
}
like image 984
oshkosh Avatar asked Oct 30 '18 19:10

oshkosh


People also ask

What does yield do in sagas?

The yielded objects are a kind of instruction to be interpreted by the middleware. When a Promise is yielded to the middleware, the middleware will suspend the Saga until the Promise completes.

What is yield call in Saga?

In this above example, we yield call . call is a blocking effect creator. This means that the saga will not continue to run to the next yield until the API call finishes. Once it's finished, we yield put . put is dispatching a new action with the result from the previous yield.

What is takeEvery in Redux saga?

The most common takeEvery function is very similar to redux-thunk in its behaviour and methodology. It's basically a wrapper for yield take of a pattern or channel and yield fork .

Does saga run before reducer?

Redux Saga as Middleware Redux Saga is a middleware that takes over the control of you actions before reaching the reducer directly.


1 Answers

1) To wait for multiple call effects to run to completion:

yield all([
    call(saga2, arg1, arg2, ...),
    call(saga3, arg1, arg2, ...)
]);

2) To dispatch multiple actions and wait for their success actions to be dispatched:

yield put(action1());
yield put(action2());

yield all([
    take(ACTION_1_SUCCESS),
    take(ACTION_2_SUCCESS)
]);

Edits responding to comments

If you call the sagas directly with all (#1 above), then you can catch errors conventionally

try {
    yield all([
        call(saga2, arg1, arg2, ...),
        call(saga3, arg1, arg2, ...)
    ]);
} catch (e) {
    // ...
}

But if a saga puts actions that other sagas listen on, that saga does not receive those exceptions. Saga1 is not a parent of or attached to those sagas. It just dispatches actions, and some other tasks elsewhere listen and respond.

For Saga1 to be aware of errors in those sagas, the sagas should not throw errors, but instead dispatch an action with an error payload:

function* saga2(action) {
    try {
        const result = yield call(...);
        yield put(action2Success(result));
    } catch (e) {
        yield put(action2Failure(e.message));
    }
}

A saga that triggers saga2 (via put(action2())) can handle success and failure:

function* saga1(action) {
    yield put(action2());
    yield put(action3());

    const [success, failure] = yield race([
        // if this occurs first, the race will exit, and success will be truthy
        all([
            take(ACTION_2_SUCCESS),
            take(ACTION_3_SUCCESS)
        ]),

        // if either of these occurs first, the race will exit, and failure will be truthy
        take(ACTION_2_FAILURE),
        take(ACTION_3_FAILURE)
    ]);

    if (failure) {
        return;
    }

    // ...
}

Sagas should handle exceptions and update the store with an error state, not throw errors. Throwing errors in sagas gets messy when working with saga concurrency constructs. For example, you cannot directly catch an error thrown by a forked task. Also, using actions to signal saga results keeps a good event log in your store, on which other sagas/reducers can respond to. When you call other sagas, the action that is supposed to initiate that saga (e.g. takeEvery(THE_ACTION, ...)) doesn't get dispatched.

like image 161
brietsparks Avatar answered Sep 18 '22 00:09

brietsparks