Previous: Cancelation

Next: Child Tasks

Errors vs Cancelation

When you yield a promise or async function call, your task function will pause execution until one of three things happens:

  1. The promise fulfills/async function returns, and your task will continue executing from that point.
  2. The promise rejects/async function throws, and your task will automatically `throw` an error from that point.
  3. Something causes the task to be canceled, which has the behavior described below.

The Task Function Syntax docs demonstrates how you can use standard JavaScript try/catch blocks to catch exceptions thrown when you yield a rejecting promise, but what about cancelation? Are cancelations considered exceptions/errors, or something else?

In ember-concurrency, cancelation is considered a third kind of "completion" (the other two being a successful return from a function, and throwing an exception from a function). Specifically, this means that if a task is canceled while it is paused on a yield, the task will essentially return from that point, it will skip any catch(e) {} blocks it is in, but it will execute any finally {} blocks. The benefit of this behavior is that:

  1. finally blocks will always run and can be used for cleanup logic *
  2. You don't have to distinguish between cancelation and thrown exceptions in your catch blocks (which you'd annoyingly have to do if cancelation were considered just another type of error).

* While finally blocks are nice for cleanup logic, make sure you're leveraging the power of Task Modifiers and .isRunning / .isIdle task properties as much as possible so that you're not manually re-implementing a lot of the implicit state that ember-concurrency provides you for free, e.g. you should avoid manually toggling the visibility of a loading spinner within a task if you could accomplish the same thing using the .isRunning property on a task.

Example

Both of the buttons below will (re)start myTask when clicked. If you click the buttons quickly, it will cause the currently running task to cancel from the yield where it is paused. Notice how cancelations don't increment the numErrors property because cancelations skip the catch block.

  • Task State: idle
  • Completions: 0
  • Errors: 0
  • Finally block runs: 0
<button {{on "click" (perform this.myTask false)}} type="button">
  Run to Completion
</button>

<button {{on "click" (perform this.myTask true)}} type="button">
  Throw an Error
</button>

<ul>
  <li>Task State: {{this.myTask.state}}</li>
  <li>Completions: {{this.numCompletions}}</li>
  <li>Errors: {{this.numErrors}}</li>
  <li>Finally block runs: {{this.numFinallys}}</li>
</ul>
export default class ErrorVsCancelationController extends Controller {
  numCompletions = 0;
  numErrors = 0;
  numFinallys = 0;

  @restartableTask *myTask(doError) {
    try {
      yield timeout(1000);
      if (doError) {
        throw new Error('Boom');
      }
    } catch (e) {
      this.incrementProperty('numErrors');
    } finally {
      this.incrementProperty('numFinallys');
    }
    this.incrementProperty('numCompletions');
  }
}




Previous: Cancelation

Next: Child Tasks