redux-saga
call effect
promises
JavaScript
asynchronous programming

Should I always use redux-saga call effect for functions that return promise?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

In Redux Saga, you can either yield a Promise directly or yield a call effect that invokes a Promise-returning function. Both approaches can work, but they are not equally maintainable in team codebases. In practice, call should be your default for async side effects because it keeps saga logic declarative and test-friendly.

What call Actually Buys You

call wraps invocation details into an effect description. This makes generator behavior explicit and easier to assert in unit tests.

javascript
1import { call, put } from "redux-saga/effects";
2
3function fetchUsersApi() {
4  return fetch("/api/users").then((res) => {
5    if (!res.ok) throw new Error("request failed");
6    return res.json();
7  });
8}
9
10export function* fetchUsersSaga() {
11  try {
12    const users = yield call(fetchUsersApi);
13    yield put({ type: "USERS_FETCH_OK", payload: users });
14  } catch (err) {
15    yield put({ type: "USERS_FETCH_FAIL", error: String(err) });
16  }
17}

With this pattern, tests can verify yielded effects without executing real network calls.

javascript
1import { call, put } from "redux-saga/effects";
2import { fetchUsersSaga } from "./sagas";
3
4test("fetchUsersSaga success", () => {
5  const gen = fetchUsersSaga();
6
7  expect(gen.next().value).toEqual(call(expect.any(Function)));
8  expect(gen.next([{ id: 1 }]).value).toEqual(
9    put({ type: "USERS_FETCH_OK", payload: [{ id: 1 }] })
10  );
11});

Yielding Raw Promises

Saga can resolve raw Promises yielded from generator functions.

javascript
1import { put } from "redux-saga/effects";
2
3export function* rawPromiseSaga() {
4  const users = yield fetch("/api/users").then((res) => res.json());
5  yield put({ type: "USERS_FETCH_OK", payload: users });
6}

This works at runtime, but the side effect boundary is less explicit. It is harder to enforce consistent testing style when some sagas yield effects and others yield raw Promises.

Practical Rule for Team Consistency

A useful guideline:

  • use call for async operations and impure functions
  • call pure synchronous helpers directly
  • keep style consistent across the repo

Example with pure function called directly:

javascript
1import { put } from "redux-saga/effects";
2
3function normalizeName(input) {
4  return input.trim().toLowerCase();
5}
6
7export function* saveNameSaga(action) {
8  const normalized = normalizeName(action.payload.name);
9  yield put({ type: "NAME_SAVE", payload: normalized });
10}

This keeps saga code concise without wrapping every tiny function in effects.

Cancellation and Abort Handling

Cancellation logic is clearer when async calls are isolated and explicit. Combine call, cancelled, and AbortController for fetch flows.

javascript
1import { call, cancelled } from "redux-saga/effects";
2
3function* uploadFileSaga(file) {
4  const controller = new AbortController();
5
6  try {
7    const response = yield call(fetch, "/api/upload", {
8      method: "POST",
9      body: file,
10      signal: controller.signal,
11    });
12
13    if (!response.ok) {
14      throw new Error("upload failed");
15    }
16
17    return yield call([response, response.json]);
18  } finally {
19    if (yield cancelled()) {
20      controller.abort();
21    }
22  }
23}

The intent is obvious: one effect starts I/O, another handles response parsing, and cancellation cleanup is deterministic.

Composability with Other Effects

call composes naturally with race, all, retry, and custom wrappers for telemetry.

javascript
1import { call, race, delay } from "redux-saga/effects";
2
3function* withTimeout(apiFn, ms) {
4  const result = yield race({
5    data: call(apiFn),
6    timeout: delay(ms),
7  });
8
9  if (result.timeout) {
10    throw new Error("timeout");
11  }
12
13  return result.data;
14}

Keeping async work in call effects makes these patterns straightforward and consistent.

When Direct Promise Yield Is Acceptable

For very small, isolated experiments, direct Promise yield is fine. The issue is not correctness; it is long-term readability in production code. If multiple developers touch sagas, consistency usually matters more than saving one line.

If you decide to allow both styles, document the rule clearly in contribution guidelines and lint conventions.

Common Pitfalls

  • Mixing raw Promise yields and call effects arbitrarily across files.
  • Wrapping every synchronous helper in call, which adds noise without benefit.
  • Assuming call alone handles cancellation without explicit abort logic.
  • Testing full network behavior in saga unit tests instead of asserting yielded effects.
  • Ignoring team conventions and creating multiple incompatible saga patterns.

Summary

  • You do not always need call, but it should be the default for Promise-returning side effects.
  • 'call improves declarative style, composability, and unit testing clarity.'
  • Keep pure synchronous helpers as direct calls.
  • Pair async call usage with explicit cancellation and timeout strategy.
  • Prioritize one consistent saga style across the codebase.

Course illustration
Course illustration

All Rights Reserved.