FAQ & Fact Sheet

Decorators

Previous versions of Ember Concurrency recommended decorators-based APIs for declaring tasks, but these APIs are no longer recommended due to decorators not playing nicely with TypeScript-based APIs.

If you would like to see the older APIs, please check out the old docs site.

Why am I seeing TaskCancelation errors in my logs?

See this link.

Does ember-concurrency work with older browsers?

In general, yes, but they're not officially supported or tested on, so your mileage may vary. The ES6 Generator Function syntax that ember-concurrency relies on can be automatically transpiled using Babel and regenerator. You may need to experiment with your config/targets.js and/or ember-cli-babel settings to include the regenerator runtime and proper transpilation for your needs.

ember-concurrency v2.0.0 uses WeakMap for implementing decorators and encapsulated tasks, so certain older browsers may require polyfills to use these features.

Encapsulated tasks also use Proxy, which may not be polyfillable, so this feature may not be available.

How do ember-concurrency Tasks compare to...

Promises and Async functions?

Async functions are just syntax sugar for Promises. Promises aren't cancelable, and as of November 2020, there is no active TC39 specification under development for adding cancelation to promises. Tasks, in contrast, are cancelable.

The design of Promises is such that once a Promise has been created, it's not possible to externally reach in and resolve/reject a Promise. This constraint encourages a clear, unidirectional, structured architecture for building asynchronous chains of logic.

Like Promises, the return/reject value of an EC Task cannot be externally set / overridden; in other words, once a Task has been performed, there's no way to externally force it to return/reject early, with the exception of cancelation. When you .cancel() a task instance, the task will "return" from wherever it is currently paused (e.g. at a await). Presently, there is no API to "delay" the cancelation of a Task once a cancel has been requested, but this functionality might be added to future APIs.

Observables?
  • Cancelability Both Observable subscriptions and Task instances are cancelable. Observables can be canceled implicitly when all subscribers have unsubscribed, or explicitly by calling .dispose() (though this approach is frowned up in favor of explicitly specifying when an observable should terminate -- e.g. via takeUntil()). Tasks can be canceled implicitly (via 1. host object destruction, 2. the restartable task modifier) or explicitly (via .cancel() / .cancelAll()), though, like Observables, the implicit approach is preferable.
  • Multiple / Single Values Observables can emit multiple values over time. Tasks don't really "emit" values, but can .set() values on the object the task lives on so that those values can be easily displayed in the template. Tasks also expose the value / error returned from the task function (see Derived State), which are preferable to .set()s where possible.
  • Composability The degree to which an abstraction "composes" depends highly on the problems it is trying to solve and the constraints therein. Observables as a low-level primitive aren't designed to solve specific problems but rather to be as flexible and composable as possible so that developers can build almost anything on top of them to model just about any domain logic under the sun. So, in a sense, Observables can be considered more composable (compositional?) than Tasks, which, by the time they exist, are bound to the lifetime of the object they live on, which constrains how and where Tasks can be used, but this tradeoff is deliberate and enables some of the patterns described below.
  • Marble Diagrams vs Block Diagrams The behavior of Observables and their various combinators can be visualized by Marble Diagrams, where each marble represents a discrete event emitted. Tasks don't emit multiple values, and instead make a greater emphasis on the concept of task/object lifespans, hence are more easily visualized as possibly overlapping blocks over time.
  • Readability Warning: highly subjective. ember-concurrency tasks are designed to "feel" like an Ember-y API; among other design goals, it should be relatively easy for a newcomer to the code base come along and understand the flow and various steps taken from the start to the end of the task. It is the contention of @machty, who is biased but also well-versed in both observables and tasks, that understanding the flow of an ember-concurrency task is much "easier" than untangling a mess of Observable combinators for an observable that does the same amount of work and exposes the same amount of derived state as an ember-concurrency task (and @machty has written plenty of apps with Observables). This clarity is due to 1) the pausable/resumable nature of the generator function syntax that ember-concurrency tasks use, 2) the fact that tasks expose a lot of commonplace derivable state that you'd otherwise have to split out / filter / merge yourself using observables (e.g. .isIdle / .isRunning / .numRunning) and 3) it's often easier to model domain state as bindable values rather than discrete events.