Catching a Failed Promise
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeWhat 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
:
// ... 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()
:
// ... 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:
// ... 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()
:
// ... 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()
:
// ... 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()
:
// ... 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:
// ... 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')
:
// ... 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:
// ... 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
:
// ... 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()
.
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.