Handling Exceptions in Redux-Saga
redux-saga
manages application side effects (async data fetching, browser storage, etc.). It aims to make side effects easier to manage, more efficient to run, simpler to test, and more resilient when things go wrong.Inevitably, effects can throw. For instance, an API call may fail. How should we handle these errors? That’s the topic here.
What If We Ignore Errors?
Consider an effect that calls the backend three times in sequence:
If the second getBooks
request fails, will “step 2” print?
It doesn’t. Once the request throws, the effect aborts immediately.
Handling a Single Request
Wrap the effect in try…catch
:
Catching the error swallows it, allowing subsequent steps to run. The console still prints step1
, step2
, and step3
.
What Counts as an Exception?
HTTP 200, 400, 404, 500…which ones trigger catch
?
The demo uses Axios. The key is validateStatus
:
validateStatus: function validateStatus(status) {
return status >= 200 && status < 300;
}
module.exports = function settle(resolve, reject, response) {
const validateStatus = response.config.validateStatus;
if (!validateStatus || validateStatus(response.status)) {
resolve(response);
} else {
reject(createError(
'Request failed with status code ' + response.status,
response.config,
null,
response.request,
response
));
}
}
Responses outside 200–299
fall into the catch
. 300-series responses trigger browser redirects before Axios resolves.
Axios source: defaults.js
Preventing a Saga Tree Crash
If a single effect throws, the watcher stops, potentially taking down the entire saga tree. To keep the app safe, wrap each effect.
import { call } from 'redux-saga/effects';
export function safe(sagaFn) {
return function* (action) {
try {
return yield call(sagaFn, action);
} catch (e) {
console.error('[react-demo | Saga Unhandled Exception] This error should be fixed or guarded in the saga.');
console.error(e);
}
};
}
Apply the Wrapper
When an Effect Throws, the Watcher Stops
Suppose the component dispatches the same action twice:
Only one log entry appears:
Build a Reusable Wrapper
To avoid repeating try…catch
, wrap the effect before registering it:
import { call } from 'redux-saga/effects';
export function safe(sagaFn) {
return function* (action) {
try {
return yield call(sagaFn, action);
} catch (e) {
console.error('[react-demo | Saga Unhandled Exception] This error should be fixed or guarded in the saga.');
console.error(e);
}
};
}
Then wire it up:
function* mySaga() {
yield takeEvery('USER_FETCH', fetchUserEffects);
yield takeEvery('TEST_SAGA', safe(testSagaEffects));
yield takeEvery('TEST_SAGA', testSagaEffects2);
}
Result
Even if the first effect fails, the second still runs.
Handling Errors Globally?
Wrapping every effect works but feels repetitive. Can we automate it?
Yes—use effectMiddlewares
:
const effectMiddleware = (next) => (effect) => {
if (effect.type === 'FORK') {
effect.payload.args[1] = safe(effect.payload.args[1]);
}
return next(effect);
};
export const sagaMiddleware = createSagaMiddleware({
effectMiddlewares: [effectMiddleware]
});
Now you don’t need to decorate each effect manually—it’s handled once.
Heads-Up
effectMiddlewares
arrived in redux-saga
1.0.0. Upgrade if you’re on an older version.
Release notes: v1.0.0
Why FORK
?
- Saga effects include types such as
TAKE
,PUT
,ALL
,FORK
, etc. takeEvery
andtakeLatest
produceFORK
effects under the hood.
Error Messages Feel Sparse?
Yes—the logs only show the action name, not the exact code location inside the effect.
Final Thoughts
- Wrapping effects keeps watchers alive, but ask why the error happens. Hiding it doesn’t make the app safer. Often it’s better to fix the root cause.
- Libraries like saga are powerful, but every abstraction comes with trade-offs. Learn the internals and build custom helpers when needed.