Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at cf.Objective() 2009 (Minneapolis, MN) with: Jared Rypka-Hauer
Ben Nadel at cf.Objective() 2009 (Minneapolis, MN) with: Jared Rypka-Hauer@ArmchairDeity )

Breaking Out Of A Promise Chain In AngularJS

By Ben Nadel on

Yesterday, I was talking to Rob Wettlaufer and Ernie Casilla - two of our lead Node.js engineers - about asynchronous control flow in Node.js. As you can imagine, InVision App does a ton of asynchronous processing of files; and, as I was we reviewing some of that asynchronous code, I came across a use of Promises that tickled my brain a bit. In the code, an error was being thrown as a means to break out of a particular promise chain. Now, you know that I love using errors to break out of a control flow; but, there was something about this particular use-case that wasn't sitting quite right with me. After a bit of noodling, I realized that it had to do with my mental model of "happy paths" in a Promise chain.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

When I think about a set of asynchronous steps in a Promise chain, I generally think about the full execution of that workflow as being the "happy path" of the promise chain execution. Meaning, the intent of the promise chain is to execute fully; and, any failure to do so is an exception to what is expected. As such, in many cases, throwing an error or returning a rejection is the proper way to "break out" of a Promise chain. In such cases, the error truly represents an "exceptional case."


 
 
 

 
 Breaking out of a promise chain when there is only one happy path. 
 
 
 

But, sometimes, the "happy path" of a Promise chain can be unclear because the Promise chain doesn't contain a single workflow. If the Promise chain contains optional or conditional asynchronous branches, each branch has its own "happy path". And, throwing an error or returning a rejection is done to "break out" of the contextual "happy path", not necessarily the entire promis chain:


 
 
 

 
 Breaking out of a promise chain when there are multiple happy paths due to optional branches of the control flow. 
 
 
 

To explore this concretely, imagine a scenario in which I want to throw water balloons at a friend. But, being that I'm a nice guy, I only want to do so on a day that has no significance to my friend. Meaning, if its her birthday or her wedding anniversary, I don't want to ruin it with water balloons. But, of course, any other day is fair game.

An asynchronous workflow for such a plan might have the following steps:

  • wakeUp
  • selectFriend
  • getSignificanceOfDay
  • determineIfShouldProceedWithPlan
  • locateFriend
  • throwWaterBalloons
  • runAway
  • gotoSleep

At first, it might seem like this workflow and Promise chain only has one happy path. But, consider the scenario in which its the selected friend's birthday and I don't want to be "that guy" so I don't throw water balloons. In that case, the workflow should look something like this:

  • wakeUp
  • selectFriend
  • getSignificanceOfDay
  • determineIfShouldProceedWithPlan
  • -- ( SKIP ) -- locateFriend
  • -- ( SKIP ) -- throwWaterBalloons
  • -- ( SKIP ) -- runAway
  • gotoSleep

When you see the optional part of the Promise chain skipped, it's easier to see that there are, in fact, multiple happy paths in this workflow: one for the main branch and one for the optional branch. I can skip the optional branch without considering the main branch to be an exceptional case. As such, I shouldn't be using an error or a rejection as a means to skip the optional portion of the workflow. After all, skipping the optional workflow is not an exception but, rather, an adherence to the expected flow of control given the circumstances.

The nice thing about a Promise chain with a single happy path is that it can be very easy to read, especially when the steps are factored out into their own functions. But, I think that you can find ways to make a Promise chain with multiple / optional happy paths easy to read as well using the same techniques. To explore this, let's take the above water balloon workflow and look at it in an AngularJS demo. The demo has two links - one link that executes the workflow as if it was one happy path and one link that executes the workflow as if it were two happy paths. I think that you'll find both approaches easy to reason about.

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Breaking Out Of A Promise Chain In AngularJS
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./demo.css"></link>
  • </head>
  • <body ng-controller="AppController">
  •  
  • <h1>
  • Breaking Out Of A Promise Chain In AngularJS
  • </h1>
  •  
  • <p>
  • <a ng-click="runTestWithNoBranch()">Run Test With No Branch</a>.
  • </p>
  •  
  • <p>
  • <a ng-click="runTestWithBranchLogic()">Run Test With Branch Logic</a>.
  • </p>
  •  
  • <!-- Load scripts. -->
  • <script type="text/javascript" src="../../vendor/angularjs/angular-1.4.7.min.js"></script>
  • <script type="text/javascript">
  •  
  • // Create an application module for our demo.
  • angular.module( "Demo", [] );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I control the root of the application.
  • angular.module( "Demo" ).controller(
  • "AppController",
  • function provideAppController( $scope, $q, $log ) {
  •  
  • // We are using counters to "randomly" break and branch logic.
  • var i = 0;
  • var j = 0;
  •  
  • // I execute the asynchronous list of tasks as if the list consisted
  • // of only a single possible "happy path" flow of control. With this
  • // kind of a mental model, the appropriate way to "break out" of a
  • // promise chain is to throw or return an error in one of the steps.
  • // This will skip directly to the next catch() statement, which in our
  • // case, is at the end of the promise chain.
  • // --
  • // CAUTION: This approach makes sense if there truly is ONLY ONE
  • // "happy path"; but, in this demo, that is not the case. As such, I
  • // think that this particular approach is flawed IN THIS CONTEXT.
  • $scope.runTestWithNoBranch = function() {
  •  
  • var promise = $q.when()
  • .then( wakeUp )
  • .then( selectFriend )
  • .then( getSignificanceOfDay )
  • .then( determineIfShouldProceedWithPlan )
  • .then( locateFriend )
  • .then( throwWaterBalloons )
  • .then( runAway )
  • .then( gotoSleep ) // CAUTION: This gets skipped-over sometimes.
  • .catch(
  • function handleReject( reason ) {
  •  
  • // Check to see if the reason for the error is that we
  • // were purposefully "breaking out" of the promise chain.
  • if ( reason.message === "SignificantDay" ) {
  •  
  • $log.warn( "Skipped to the end of promise chain." );
  •  
  • }
  •  
  • $log.warn( "Only Catch:", reason );
  •  
  • }
  • )
  • ;
  •  
  • return( promise );
  •  
  •  
  • // Individual steps.
  •  
  •  
  • function wakeUp() {
  •  
  • $log.info( "- - - - - - - - - - - - - - - - - - -" );
  • $log.info( "- - - - - !! Staring Day !! - - - - -" );
  • $log.info( "- - - - - - - - - - - - - - - - - - -" );
  • $log.info( "wakeUp()" );
  •  
  • }
  •  
  •  
  • function selectFriend() {
  •  
  • $log.info( "selectFriend()" );
  •  
  • }
  •  
  •  
  • function getSignificanceOfDay() {
  •  
  • $log.info( "getSignificanceOfDay()" );
  •  
  • // Determine if the current day is "significant" for the selected
  • // friend. Meaning, is it something like a Birthday or a wedding
  • // anniversary - something where it would be considered poor taste
  • // to hassle this person.
  • return( ! ( ++i % 3 ) );
  •  
  • }
  •  
  •  
  • function determineIfShouldProceedWithPlan( isSignificantDay ) {
  •  
  • $log.info( "determineIfShouldProceedWithPlan()" );
  •  
  • // If this IS a significant day, we DO NOT want to hassle this
  • // person. As such, return a Rejection so that we can break out
  • // of the current workflow without proceeding with the plan.
  • if ( isSignificantDay ) {
  •  
  • return( $q.reject( new Error( "SignificantDay" ) ) );
  •  
  • }
  •  
  • }
  •  
  •  
  • function locateFriend() {
  •  
  • $log.info( "locateFriend()" );
  •  
  • // If we couldn't locate the friend, return a rejection to
  • // indicate a failure to fully execute on the plan.
  • if ( ! ( ++j % 3 ) ) {
  •  
  • return( $q.reject( new Error( "NotFound" ) ) );
  •  
  • }
  •  
  • }
  •  
  •  
  • function throwWaterBalloons() {
  •  
  • $log.info( "throwWaterBalloons()" );
  •  
  • }
  •  
  •  
  • function runAway() {
  •  
  • $log.info( "runAway()" );
  •  
  • }
  •  
  •  
  • function gotoSleep() {
  •  
  • $log.info( "gotoSleep()" );
  •  
  • }
  •  
  • };
  •  
  •  
  • // I execute the asynchronous list of tasks as if the list consisted of
  • // a primary branch of logic and a secondary branch of logic, each of
  • // which has its own decoupled "happy path" flow of control. With this
  • // kind of a mental model, we can still throw or return an error when
  • // we need to break based on a FAILURE TO EXECUTE THE HAPPY PATH; however,
  • // we DO NOT need to use errors or rejection to skip the embedded branch
  • // logic. Branch logic, if inappropriate, is simply skipped in the parent
  • // control flow.
  • $scope.runTestWithBranchLogic = function() {
  •  
  • var promise = $q.when()
  • .then( wakeUp )
  • .then( selectFriend )
  • .then( getSignificanceOfDay )
  • .then( determineIfShouldProceedWithPlan )
  • .then( gotoSleep )
  • .catch(
  • function handleReject( reason ) {
  •  
  • $log.warn( "Primary Catch:", reason );
  •  
  • }
  • )
  • ;
  •  
  • return( promise );
  •  
  •  
  • // Individual Steps.
  •  
  •  
  • function wakeUp() {
  •  
  • $log.info( "- - - - - - - - - - - - - - - - - - -" );
  • $log.info( "- - - - - !! Staring Day !! - - - - -" );
  • $log.info( "- - - - - - - - - - - - - - - - - - -" );
  • $log.info( "wakeUp()" );
  •  
  • }
  •  
  •  
  • function selectFriend() {
  •  
  • $log.info( "selectFriend()" );
  •  
  • }
  •  
  •  
  • function getSignificanceOfDay() {
  •  
  • $log.info( "getSignificanceOfDay()" );
  •  
  • // Determine if the current day is "significant" for the selected
  • // friend. Meaning, is it something like a Birthday or a wedding
  • // anniversary - something where it would be considered poor taste
  • // to hassle this person.
  • return( ! ( ++i % 3 ) );
  •  
  • }
  •  
  •  
  • function determineIfShouldProceedWithPlan( isSignificantDay ) {
  •  
  • $log.info( "determineIfShouldProceedWithPlan()" );
  •  
  • // If this IS a significant day, we DO NOT want to hassle this
  • // person. As such, return out without walking down the optional
  • // branch of logic.
  • // --
  • // NOTE: We are NOT considering this an error since it does not
  • // affect the "happy path" of the primary flow of control; it is
  • // simply skipping the optional, asynchronous branch.
  • if ( isSignificantDay ) {
  •  
  • $log.warn( "Today is significant - don't punk friend." );
  • return;
  •  
  • }
  •  
  • // If this day is NOT significant, let's branch away from the
  • // main control flow into the "punk your friend" branch of
  • // asynchronous tasks.
  • return( optionalPunkFriendAsyncBranch() );
  •  
  • }
  •  
  •  
  • // This "step" in the asynchronous control flow is actually an entire
  • // branch of logic that has its own steps and error handler. On its
  • // own, it represents a single "happy path" of actions.
  • function optionalPunkFriendAsyncBranch() {
  •  
  • $log.info( "optionalPunkFriendAsyncBranch()" );
  •  
  • var promise = $q.when()
  • .then( locateFriend )
  • .then( throwWaterBalloons )
  • .then( runAway )
  • .catch(
  • function handleReject( reason ) {
  •  
  • $log.warn( "Branch Catch:", reason );
  •  
  • }
  • )
  • ;
  •  
  • return( promise );
  •  
  •  
  • function locateFriend() {
  •  
  • $log.info( "locateFriend()" );
  •  
  • // If we couldn't locate the friend, return a rejection to
  • // indicate a failure to fully execute on the plan.
  • if ( ! ( ++j % 3 ) ) {
  •  
  • return( $q.reject( new Error( "NotFound" ) ) );
  •  
  • }
  •  
  • }
  •  
  •  
  • function throwWaterBalloons() {
  •  
  • $log.info( "throwWaterBalloons()" );
  •  
  • }
  •  
  •  
  • function runAway() {
  •  
  • $log.info( "runAway()" );
  •  
  • }
  •  
  • } // END: Asynchronous Branch - optionalPunkFriendAsyncBranch.
  •  
  •  
  • function gotoSleep() {
  •  
  • $log.info( "gotoSleep()" );
  •  
  • }
  •  
  • };
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

As you can see, in the first approach, where we treat the Promise chain a single happy path, the steps are really easy to read. However, the way we have it setup, skipping the water balloon event also [accidentally] skips the gotoSleep() step.

In the second approach, where we think about the Promise chain as containing two happy paths - one primary and one optional - we can skip only the optional parts while maintaining a top-down execution plan. In such a workflow, we can still use errors and rejections to break out of a "happy path"; but, in doing so, we only break out of the "local" happy path, not necessarily the entire Promise chain.

Promises are seriously awesome. And, they make working with asynchronous workflows a whole lot easier. But, they are still complicated and they still require a lot of thought. A big step for me was realizing that one Promise chain doesn't necessarily map to one "happy path." And, when I embraced this, I found it much easier to think about managing conditional control flows.




Reader Comments

Nice article! What do you use to draw your diagrams?

Depending on which promises implementation you're using, you may have access to some additional tools such as .finally() and .catch(MyCustomError) that can give you finer-grained control over complex promise chains.

Reply to this Comment

@Jonathon,

Thanks! I use Adobe Fireworks to do all of my illustrations. I absolutely love it, but it's been end-of-lifed. I'll have to start learning Sketch one of these days.

I've only read about BlueBird, but the idea of having a catch-handler for a specific type of error is really interesting. That actually lines up more closely with how my synchronous code works with multiple Catch-blocks on the server.

Mostly I deal with Q since AngularJS has a "flavor" of $q out of the box.

Reply to this Comment

Yeah, using the .catch(MyCustomError, MyCustomErrorHandler) syntax with BlueBird you can basically set 'exit points' so to speak within the promise chains, almost like a 'break' or even a 'goto'. You do have to make sure you are persisting the state you'll need to properly handle the promise rejection however, as the catch handler only has access to the returned error object for scope unless explicitly bound otherwise. BlueBird makes that pretty easy by being able to .bind( this ) across the entire promise chain like:

this.someValue = calculateSomeClassThing( context );
return promiseChain()
.bind( this )
.then( this.fn1UsingSomeValue )
.then( this.fn2UsingSomeValue )
.then( this.fn3UsingSomeValue )
.catch( errors.SkipChain, function( err ) { /* we have access to this.someValue */})
.catch( errors.Error, this.handleGenericError )

this.handleGenericError = function( err ) { this.someValue(err) };

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.