Catching a Failed Promise

What about handling failures? As you can see in the Promise documentation, the .then() function has an optional second argument: a function that will be called on failure. In other words, we can go to the end of .then() and add a function. We know that the value passed to jQuery failures is the jqXHR. Let's console.log('failed') and also log jqXHR.responseText:

176 lines web/assets/js/RepLogApp.js
... lines 1 - 2
(function(window, $, Routing) {
... lines 4 - 26
$.extend(window.RepLogApp.prototype, {
... lines 28 - 79
handleNewFormSubmit: function(e) {
... lines 81 - 100
}).then(function(data) {
... lines 102 - 105
}, function(jqXHR) {
console.log('failed!');
console.log(jqXHR.responseText);
}).then(function(data) {
... lines 110 - 111
})
},
... lines 114 - 155
});
... lines 157 - 174
})(window, jQuery, Routing);

Ok, refresh! Keep the form blank and submit. Ok cool! It did call our failure handler and it did print the responseText correctly.

Standardizing around .catch

The second way - and better way - to handle rejections, is to use the .catch() function. Both approaches are identical, but this is easier for me to understand. Instead of passing a second argument to .then(), close up that function and then call .catch():

176 lines web/assets/js/RepLogApp.js
... lines 1 - 2
(function(window, $, Routing) {
... lines 4 - 26
$.extend(window.RepLogApp.prototype, {
... lines 28 - 79
handleNewFormSubmit: function(e) {
... lines 81 - 100
}).then(function(data) {
... lines 102 - 105
}).catch(function(jqXHR) {
console.log('failed!');
console.log(jqXHR.responseText);
}).then(function(data) {
... lines 110 - 111
})
},
... lines 114 - 155
});
... lines 157 - 174
})(window, jQuery, Routing);

This will do the exact same thing as before.

Catch Recovers from Errors

But in both cases, something very weird happens: the second .then() success handler is being called. Wait, what? So the first .then() is being skipped, which makes sense, because the AJAX call failed. But after .catch(), the second .then() is being called. Why?

Here's the deal: catch is named catch for a reason: you really need to think about it in the same way as a try-catch block in PHP. It will catch the failed Promise above and return a new Promise that resolves successfully. That means that any handlers attached to it - like our second .then() - will execute as if everything was fine.

We're going to talk more about this, but obviously, this is probably not what we want. Instead, move the .catch() to the end:

176 lines web/assets/js/RepLogApp.js
... lines 1 - 2
(function(window, $, Routing) {
... lines 4 - 26
$.extend(window.RepLogApp.prototype, {
... lines 28 - 79
handleNewFormSubmit: function(e) {
... lines 81 - 100
}).then(function(data) {
... lines 102 - 105
}).then(function(data) {
... lines 107 - 108
}).catch(function(jqXHR) {
console.log('failed!');
console.log(jqXHR.responseText);
});
},
... lines 114 - 155
});
... lines 157 - 174
})(window, jQuery, Routing);

Now, the second .then() will only be executed if the first .then() is executed. The .catch() will catch any failed Promises - or errors - at the bottom. More on the error catching later.

Refresh now! Cool - only the catch() handler is running.

Refactoring Away from success

Ok, with our new Promise powers, let's refactor our success and error callbacks to modern and elegant, promises.

To do that, just copy our code from success into .then():

161 lines web/assets/js/RepLogApp.js
... lines 1 - 2
(function(window, $, Routing) {
... lines 4 - 26
$.extend(window.RepLogApp.prototype, {
... lines 28 - 77
handleNewFormSubmit: function(e) {
... lines 79 - 86
$.ajax({
... lines 88 - 90
}).then(function(data) {
self._clearForm();
self._addRow(data);
... lines 94 - 96
});
},
... lines 99 - 140
});
... lines 142 - 159
})(window, jQuery, Routing);

I'm not worried about returning anything because we're not chaining our "then"s. Remove the second .then() and move the error callback code into .catch():

161 lines web/assets/js/RepLogApp.js
... lines 1 - 2
(function(window, $, Routing) {
... lines 4 - 26
$.extend(window.RepLogApp.prototype, {
... lines 28 - 77
handleNewFormSubmit: function(e) {
... lines 79 - 86
$.ajax({
url: $form.data('url'),
method: 'POST',
data: JSON.stringify(formData)
}).then(function(data) {
self._clearForm();
self._addRow(data);
}).catch(function(jqXHR) {
var errorData = JSON.parse(jqXHR.responseText);
self._mapErrorsToForm(errorData.errors);
});
},
... lines 99 - 140
});
... lines 142 - 159
})(window, jQuery, Routing);

With any luck, that will work exactly like before. Yea! The error looks good. And adding a new one works too.

Let's find our two other $.ajax() spots. Do the same thing there: Move the success function to .then(), and move the other success also to .then():

161 lines web/assets/js/RepLogApp.js
... lines 1 - 2
(function(window, $, Routing) {
... lines 4 - 26
$.extend(window.RepLogApp.prototype, {
... lines 28 - 31
loadRepLogs: function() {
... line 33
$.ajax({
url: Routing.generate('rep_log_list'),
}).then(function(data) {
$.each(data.items, function(key, repLog) {
self._addRow(repLog);
});
})
},
... lines 42 - 48
handleRepLogDelete: function (e) {
... lines 50 - 62
$.ajax({
url: deleteUrl,
method: 'DELETE'
}).then(function() {
$row.fadeOut('normal', function () {
$(this).remove();
self.updateTotalWeightLifted();
});
})
},
... lines 73 - 140
});
... lines 142 - 159
})(window, jQuery, Routing);

Awesome!

Why is this Awesome for me?

One of the big advantages of Promises over adding success or error options is that you can refactor your asynchronous code into external functions. Let's try it: create a new function called, _saveRepLog with a data argument:

166 lines web/assets/js/RepLogApp.js
... lines 1 - 2
(function(window, $, Routing) {
... lines 4 - 26
$.extend(window.RepLogApp.prototype, {
... lines 28 - 96
_saveRepLog: function(data) {
... lines 98 - 102
},
... lines 104 - 145
});
... lines 147 - 164
})(window, jQuery, Routing);

Now, move our AJAX code here, and return it. Set the data key to JSON.stringify(data). And for the url, we can replace this with Routing.generate('rep_log_new'):

166 lines web/assets/js/RepLogApp.js
... lines 1 - 2
(function(window, $, Routing) {
... lines 4 - 26
$.extend(window.RepLogApp.prototype, {
... lines 28 - 96
_saveRepLog: function(data) {
return $.ajax({
url: Routing.generate('rep_log_new'),
method: 'POST',
data: JSON.stringify(data)
});
},
... lines 104 - 145
});
... lines 147 - 164
})(window, jQuery, Routing);

In the controller, make sure to expose that route to JavaScript:

131 lines src/AppBundle/Controller/RepLogController.php
... lines 1 - 13
class RepLogController extends BaseController
{
... lines 16 - 60
/**
* @Route("/reps", name="rep_log_new", options={"expose" = true})
... line 63
*/
public function newRepLogAction(Request $request)
... lines 66 - 129
}

Here's the point: above, replace the AJAX call with simply this._saveRepLog() and pass it formData:

166 lines web/assets/js/RepLogApp.js
... lines 1 - 2
(function(window, $, Routing) {
... lines 4 - 26
$.extend(window.RepLogApp.prototype, {
... lines 28 - 77
handleNewFormSubmit: function(e) {
... lines 79 - 85
var self = this;
this._saveRepLog(formData)
.then(function(data) {
self._clearForm();
self._addRow(data);
}).catch(function(jqXHR) {
var errorData = JSON.parse(jqXHR.responseText);
self._mapErrorsToForm(errorData.errors);
});
},
... lines 96 - 145
});
... lines 147 - 164
})(window, jQuery, Routing);

Isolating asynchronous code like this wasn't possible before because, in this function, we couldn't add any success or failure options to the AJAX call. But now, since we know _saveRepLog() returns a Promise, and since we also know that Promises have .then() and .catch() methods, we're super dangerous. If we ever needed to save a RepLog from somewhere else in our code, we could call _saveRepLog() to do that... and even attach new handlers in that case.

Next, let's look at another mysterious behavior of .catch().

Leave a comment!

  • 2017-02-17 Victor Bocharsky

    Hi St├ęphane,

    You're right! Thank you for this report. It's already fixed.

    Cheers!

  • 2017-02-16 St├ęphane

    Hello Ryan,

    I found a mistake about the code into RepLogController.php :
    You write : @Route("/reps", name="rep_log_list", options={"expose" = true})
    but I think that is : @Route("/reps", name="rep_log_new", options={"expose" = true})

    Thank for this very interesting tuto.