Task cancellation
Once a task is forked, you can abort its execution using yield cancel(task)
. Cancelling a running task will throw a SagaCancellationException
inside it.
To see how it works, let's consider a simple example. A background sync which can be started/stopped by some UI commands. Upon receiving a START_BACKGROUND_SYNC
action, we fork a background task that will periodically sync some data from a remote server.
The task will execute continually until a STOP_BACKGROUND_SYNC
action is triggered. Then we cancel the background task and wait again for the next START_BACKGROUND_SYNC
action.
import { take, put, call, fork, cancel, SagaCancellationException } from 'redux-saga'
import actions from 'somewhere'
import { someApi, delay } from 'somewhere'
function* bgSync() {
try {
while(true) {
yield put(actions.requestStart())
const result = yield call(someApi)
yield put(actions.requestSuccess(result))
yield call(delay, 5000)
}
} catch(error) {
if(error instanceof SagaCancellationException)
yield put(actions.requestFailure('Sync cancelled!'))
}
}
function* main() {
while( yield take(START_BACKGROUND_SYNC) ) {
// starts the task in the background
const bgSyncTask = yield fork(bgSync)
// wait for the user stop action
yield take(STOP_BACKGROUND_SYNC)
// user clicked stop. cancel the background task
// this will throw a SagaCancellationException into the forked bgSync task
yield cancel(bgSyncTask)
}
}
yield cancel(bgSyncTask)
will throw a SagaCancellationException
inside the currently running task. In the above example, the exception is caught by bgSync
. Note that uncaught SagaCancellationException are not bubbled upward. In the above example, if bgSync
doesn't catch the cancellation error, the error will not propagate to main
(because main
has already moved on).
Cancelling a running task will also cancel the current effect where the task is blocked at the moment of cancellation.
For example, suppose that at a certain point in an application's lifetime, we had this pending call chain:
function* main() {
const task = yield fork(subtask)
...
// later
yield cancel(task)
}
function* subtask() {
...
yield call(subtask2) // currently blocked on this call
...
}
function* subtask2() {
...
yield call(someApi) // currently blocked on this all
...
}
yield cancel(task)
will trigger a cancellation on subtask
, which in turn will trigger a cancellation on subtask2
. A SagaCancellationException
will be thrown inside subtask2
, then another SagaCancellationException
will be thrown inside subtask
. If subtask
omits to handle the cancellation exception, a warning message is printed to the console to warn the developer (the message is only printed if there is a process.env.NODE_ENV
variable
set and it's set to 'development'
).
The main purpose of the cancellation exception is to allow cancelled tasks to perform any cleanup logic, so we wont leave the application in an inconsistent state. In the above example of background sync, by catching the cancellation exception, bgSync
is able to dispatch a requestFailure
action to the store. Otherwise, the store could be left in a inconsistent state (e.g. waiting for the result of a pending request).
It's important to remember that
yield cancel(task)
doesn't wait for the cancelled task to finish (i.e. to perform its catch block). The cancel effect behave like fork. It returns as soon as the cancel was initiated. Once cancelled, a task should normally return as soon as it finishes its cleanup logic. In some cases, the cleanup logic could involve some async operations, but the cancelled task lives now as a separate process, and there is no way for it to rejoin the main control flow (except dispatching actions for other tasks via the Redux store. However this will lead to complicated control flows that are hard to reason about. It's always preferable to terminate a cancelled task ASAP).
Automatic cancellation
Besides manual cancellation there are cases where cancellation is triggered automatically
1- In a race
effect. All race competitors, except the winner, are automatically cancelled.
2- In a parallel effect (yield [...]
). The parallel effect is rejected as soon as one of the sub-effects is rejected (as implied by Promise.all
). In this case, all the other sub-effects are automatically cancelled.