At Iora Health, our mission is to restore humanity to health care. One of the ways we serve our mission is by building a collaborative care platform centered around caring for our patients, rather than using traditional enterprise health systems that are built around administrative tasks like billing and coding. Because our namesake is a bird and because we like fun names, we call our platform Chirp.
Chirp serves many functions. It’s an electronic health records system. It’s a scheduling platform. It’s a practice management platform. It even facilitates some billing and coding processes (gasp!). Chirp is complex.
Today we’re focused on how ember-concurrency helps manage complexity and allows us to stay focused on building features that serve our mission and help our teams care for patients.
Life before ember-concurrency
The year was 2016. Half the team was focused on building a scheduling application for our practices in Ember. There was data flying everywhere! Schedules! Calendar events! Staff members! We were stuck wrangling Promises. I wrote
.then so many times, I wore out the letter
t on two keyboards. It was a nightmare.
Like many Ember users, we use Ember-Data for our data loading and management needs, so a lot of our code looked something like this:
This is a snippet of our appointment-form component, circa 2016. You’ll notice that besides doing a save of the new appointment record we’ve created, we’re also updating a few bits of state on the component:
appointmentFormErrors , and some properties on our
In the template for the component, we used
isAppointmentSaving for controlling form UI state. First, it would disable the submit button to prevent duplicate submissions. Then, it would control the display of a loading state on the appointment form, so the user knew we were altering time and space. Likewise, with the
appointmentFormErrors , we managed and cleared them out manually.
Unfortunately, the manual form state tracking was error prone. What would happen if we forgot to reset
isAppointmentSaving in the error state? What happens if they close the form or navigate away while it’s saving? The answer: bad things. Users could get stuck in the form, with the spinner spinning away until they gave up and reloaded the page. If a user navigated away, we’d get a nice big stack trace in our error reporting tool. We couldn’t do anything about it, and it simply told us “the component is gone.”
What is ember-concurrency?
ember-concurrency is a popular add-on for Ember, which provides a small but powerful set of tools for managing asynchronous work.
ember-concurrency provides a task-based approach for managing asynchronous work. At a high-level, it makes “Cancel” buttons, debouncing, throttling, secondary data loading, and other complicated things uncomplicated to implement. A
Task is a
Promise-like construct which gives you access to execution state, such as whether the task is running, is idle, or has failed and allows for setting constraints on how many instances of the task can be running. Manually tracking
isLoading and hoping that all bases are covered is no longer required. All your base are belong to
ember-concurrency now. Also, unlike Promises, they can be broken, e.g.
partyTask.cancel("sorry, just crashed on my couch. #Netflix") .
There are a lot of amazing things that are made possible by
ember-concurrency, but unfortunately I cannot describe them all in this post. Please check out the wonderful documentation site, which features in-depth explanations, fancy animations, and useful examples to get you started. You can also read Alex Matchneer’s post about it or watch his EmberConf talk.
(*Actual results may vary.)
How ember-concurrency improved things
In early 2017, we brought in Mike North to give the team a few days of Ember training. We’d recently committed to rewriting our main application from Backbone to Ember and were looking to train ourselves up, so that we had the tools and tricks to succeed. (Spoiler: it worked.) One of the most important power-ups Mike introduced us to was
We went over some of our scheduling application’s code and Mike showed us a few examples of how we could refactor our Promise nests using
ember-concurrency. The idea was to manage only what we need to manage, and leverage as much derived state as possible to reduce the potential for mistakes. The whole room cheered. We hoisted Mike up onto our shoulders and went to get pizza.
When we got back to working on our scheduling application, we were able to refactor away a lot of the manual state tracking we were doing and use the wonderful derived state from
ember-concurrency instead. We went from being professional Promise Wranglers to becoming Task Tango-ers, which sounds far more delightful. Here’s what our appointment form code looks like on Tasks:
This refactoring was cool. We’re certainly managing less state ourselves, but it doesn’t really show off how powerful
ember-concurrency can be. To get deeper into
ember-concurrency, we need to look at a more difficult problem, one that’s more hairy to solve without
Building for resiliency and recovery
Let’s say you’re a small, but growing healthcare delivery company. You’ve got your area of focus, and you’re really good at it, but you need to provide support for something very complicated — prescriptions management — that cannot be done with the resources you currently have. (Maybe you’ll get there one day. I believe in you!) You would likely engage a vendor that provides the service you’re after. However, unless you’re a really good negotiator or hold the vendor’s mascot hostage, vendors work with lots of companies, not just your own, and cannot guarantee 100% reliability. Their systems are out of your control and sometimes they experience issues. Sometimes they experience issues more frequently than you’d like.
No one can predict these things but it’s important to be prepared by building in resiliency and recovery opportunities into the UI. Topics like fault tolerance are usually discussed at the systems and network level, but there’s a place for them at the UI level too.
Handling the unhappy path
Imagine you’re using Promises, and you need to provide a level of resiliency or fault tolerance to an operation.
- You need to be able to tell the user when something went wrong.
- The user should also be given the option to retry the operation.
- Later, you’d like to be able to retry the operation automatically without any interaction from the user. You’d like to target the automatic retries on errors consistent with transient failures the vendor is known to experience.
How would you do it?
A lot of people would look at such a problem and come to the very reasonable conclusion that it’s not worth the effort. Others might tackle it with similar but domain-tailored solutions, which would then need to be maintained.
An error occurred, we’re sorry
Let’s look at the first requirement related to telling the user what went wrong. This is easy with
ember-concurrency, and in fact there are a few ways to do it: you could check the
isError property on the latest task instance in your template to show a message or you could listen for the
errored lifecycle event on the task and trigger a flash message. Here’s how we use the
isError property and the derived error state to show when there’s a problem with our prescriptions service:
This is a quick win with
ember-concurrency. We don’t have to manually track if the request failed ourselves or why it failed. It’s all attached to the task instance, and we don’t have to think about resetting any state.
Now, what about letting the user retry a request? This is real easy with
ember-concurrency too. Here’s how we added a retry button:
Admittedly, you could do this with a regular action as well. But without
ember-concurrency, you’d need to manually manage that error state and probably incorporate some sort of state reset within the retry button click. Yuck.
Extending ember-concurrency to enable automatic retries
ember-concurrency provides a really powerful level of primitives, which help reduce a lot of manual state tracking and boilerplate for common asynchronous UI patterns. However,
ember-concurrency is not immune to boilerplate when it comes to more complex patterns.
Take for example, a long-running, looping background task, in which we want to try and recover from a certain number of errors before we give up. We can solve a lot of this with the
ember-concurrency primitives we have, but end up with a lot of manual state tracking for things like “how many times has this task failed?” In our application, we have a set of tasks like this for maintaining our user’s session while continuously using the application. Here’s how we did this using only regular
Taking a look at the
keepAlivePollingTask, there is a lot going on in there. Most of the complexity is surrounding tracking state for how many requests have failed, how many have run, and adapting what happens next based on that.
The above approach does result in Working Software™. It tracks that state, shows different notifications to the user when we’re trying to reconnect them, and alerts them when they might get automatically logged-out due to inactivity. However, it is a very rigid implementation. The retry and failure tracking logic is as-is. It’s easy enough to reduce or increase the amount of failures allowed, or change the interval between checks. If we wanted to do something more complicated, like implementing exponential backoff, we’d need to revamp the state tracking and restructure some things, rather than swapping something out or changing a setting.
ember-concurrency-retryable comes in. It’s an Ember add-on that hooks into
ember-concurrency to provide support for automatic retrying of tasks. (Disclosure: I am the author of
ember-concurrency-retryable, and I alone am responsible for its faults.)
Implemented as a new task modifier (like
.restartable()), you can enable retryable behavior for any tasks and delegate control to reusable policies for how the task will be retried. Below is a simple example of how it can be added to tasks:
If at first you don’t succeed, retry, retry, retry, retry again
Refactoring the session service using
ember-concurrency-retryable, we’re able to get rid of a lot of the inner complexity in the
keepAlivePolling task. In addition, we’re able to leverage the events interface of
ember-concurrency-retryable to react to the task, rather than complicate our task function. Because
ember-concurrency-retryable is controlling the retry behavior, we can tweak and swap out the retry policy with something that fits with some future requirements, without retooling the session service code.
Below is the refactored session service. As can be seen on lines 26–46, the
keepAlivePolling task is much less complex now that we’re not tracking as much state ourselves. Instead, the logout and disconnected handlers are called in response to events according to the lifecycle
Our session service still has some oddities related to legacy behavior we need to iron out. However, it’s a much more understandable bit of code, now that we’ve been able to leverage more advanced
ember-concurrency features like events,
race, and use
ember-concurrency-retryable to refactor away the complexity around retrying.
As more application complexity moves to the frontend, having a robust way to manage asynchronous application behavior and the concurrency problems inherent in the browser is imperative in order to provide a consistent and reliable user experience. Promises and async functions are wonderful primitives, but they’re just that: primitives. A toolkit like
ember-concurrency and power-ups like
ember-concurrency-retryable provide the control you need to design interactions you can reason about, so you can stay focused on the problems you need to solve for your colleagues, users, customers, and, in our case, patients. If you’re still out there in the Async Sea, drowning in Promises, I strongly encourage you to join us aboard the USS Ember-Concurrency. We’re shipping out full-steam ahead! (I couldn’t think of anymore bird puns.)