Promise catch: Catches Errors?
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 SubscribeYay! Let's complicate things!
Our AJAX call works really well, because when we make an AJAX call to create a new RepLog
, our server returns all the data for that new RepLog
. That means that when we call .then()
on the AJAX promise, we have all the data we need to call _addRow()
and get that new row inserted!
// ... lines 1 - 2 | |
(function(window, $, Routing) { | |
// ... lines 4 - 26 | |
$.extend(window.RepLogApp.prototype, { | |
// ... lines 28 - 77 | |
handleNewFormSubmit: function(e) { | |
// ... lines 79 - 86 | |
this._saveRepLog(formData) | |
.then(function(data) { | |
self._clearForm(); | |
self._addRow(data); | |
// ... lines 91 - 93 | |
}); | |
}, | |
// ... lines 96 - 145 | |
}); | |
// ... lines 147 - 164 | |
})(window, jQuery, Routing); |
Too easy: so let's make it harder!
Making our Endpoint Less Friendly
Pretend that we don't have full control over our API. And instead of returning the RepLog
data from the create endpoint - which is what this line does - it returns an empty response:
// ... lines 1 - 13 | |
class RepLogController extends BaseController | |
{ | |
// ... lines 16 - 64 | |
public function newRepLogAction(Request $request) | |
{ | |
// ... lines 67 - 93 | |
//$response = $this->createApiResponse($apiModel); | |
$response = new Response(null, 204); | |
// ... lines 96 - 102 | |
} | |
// ... lines 104 - 130 | |
} |
Passing null
means no response content, and 204 is just a different status code used for empty responses - that part doesn't make any difference.
Now head over and fill out the form successfully. Whoa!
Yep, it blew up - that's not too surprising: we get an error that says:
totalWeightLifted
is not defined.
And if you look closely, that's coming from underscore.js
. This is almost definitely an error in our template. We pass the response data - which is now empty - into ._addRow()
:
// ... lines 1 - 2 | |
(function(window, $, Routing) { | |
// ... lines 4 - 26 | |
$.extend(window.RepLogApp.prototype, { | |
// ... lines 28 - 77 | |
handleNewFormSubmit: function(e) { | |
// ... lines 79 - 87 | |
.then(function(data) { | |
// ... line 89 | |
self._addRow(data); | |
// ... lines 91 - 93 | |
}); | |
}, | |
// ... lines 96 - 145 | |
}); | |
// ... lines 147 - 164 | |
})(window, jQuery, Routing); |
And that eventually becomes the variables for the template:
// ... lines 1 - 2 | |
(function(window, $, Routing) { | |
// ... lines 4 - 26 | |
$.extend(window.RepLogApp.prototype, { | |
// ... lines 28 - 136 | |
_addRow: function(repLog) { | |
var tplText = $('#js-rep-log-row-template').html(); | |
var tpl = _.template(tplText); | |
var html = tpl(repLog); | |
this.$wrapper.find('tbody').append($.parseHTML(html)); | |
this.updateTotalWeightLifted(); | |
} | |
}); | |
// ... lines 147 - 164 | |
})(window, jQuery, Routing); |
An empty response means that no variables are being passed. Hence, totalWeightLifted
is not defined.
But check this out: there's a second error:
JSON Exception: unexpected token
A catch Catches Errors
This is coming from RepLogApp.js
, line 94. Woh, it's coming from inside our .catch()
handler:
// ... lines 1 - 2 | |
(function(window, $, Routing) { | |
// ... lines 4 - 26 | |
$.extend(window.RepLogApp.prototype, { | |
// ... lines 28 - 77 | |
handleNewFormSubmit: function(e) { | |
// ... lines 79 - 86 | |
this._saveRepLog(formData) | |
.then(function(data) { | |
// ... lines 89 - 90 | |
}).catch(function(jqXHR) { | |
var errorData = JSON.parse(jqXHR.responseText); | |
self._mapErrorsToForm(errorData.errors); | |
}); | |
}, | |
// ... lines 96 - 145 | |
}); | |
// ... lines 147 - 164 | |
})(window, jQuery, Routing); |
Now, as we understand it, our catch
should only be called when our Promise
fails, in other words, when we have an AJAX error. But in this case, the server returns a 204 status code - that is a successful status code. So why is our catch
being called?
Here's the deal: in reality, .catch()
will be called if your Promise
is rejected, or if a handler above it throws an error. Since our .then()
calls _addRow()
and that throws an exception, this ultimately triggers the .catch()
. Again, this works a lot like the try-catch
block in PHP!
Tip
There are some subtle cases when throwing an exception inside asynchronous code
won't trigger your .catch()
. The Mozilla Promise Docs
discuss this!
Conditionally Handling in catch
So this complicates things a bit. Before, we assumed that the value passed to .catch()
would always be the jqXHR
object: that's what jQuery
passes when its Promise is rejected. But now, we're realizing that it might not be that, because something else might fail.
Let's console.log(jqXHR)
:
// ... lines 1 - 2 | |
(function(window, $, Routing) { | |
// ... lines 4 - 26 | |
$.extend(window.RepLogApp.prototype, { | |
// ... lines 28 - 77 | |
handleNewFormSubmit: function(e) { | |
// ... lines 79 - 86 | |
this._saveRepLog(formData) | |
.then(function(data) { | |
// ... lines 89 - 90 | |
}).catch(function(jqXHR) { | |
console.log(jqXHR); | |
// ... lines 93 - 94 | |
}); | |
}, | |
// ... lines 97 - 146 | |
}); | |
// ... lines 148 - 165 | |
})(window, jQuery, Routing); |
Ok, refresh and fill out our form. There it is! Thanks to the error, it logs a "ReferenceError".
We've just found out that .catch()
will catch anything that went wrong... and that the value passed to your handler will depend on what went wrong. This means that, if you want, you can code for this: if (jqXHR instanceof ReferenceError)
, then console.log('wow!')
:
// ... lines 1 - 2 | |
(function(window, $, Routing) { | |
// ... lines 4 - 26 | |
$.extend(window.RepLogApp.prototype, { | |
// ... lines 28 - 77 | |
handleNewFormSubmit: function(e) { | |
// ... lines 79 - 86 | |
this._saveRepLog(formData) | |
.then(function(data) { | |
// ... lines 89 - 90 | |
}).catch(function(jqXHR) { | |
if (jqXHR instanceof ReferenceError) { | |
console.log('wow!'); | |
} | |
// ... lines 95 - 96 | |
}); | |
}, | |
// ... lines 99 - 148 | |
}); | |
// ... lines 150 - 167 | |
})(window, jQuery, Routing); |
Let's see if that hits! Refresh, lift some laptops and, there it is!
What JavaScript doesn't have is the ability to do more intelligent try-catch block, where you catch only certain types of errors. Instead, .catch()
handles all errors, but then, you can write your code to be a bit smarter.
Since we really only want to catch jqXHR
errors, we could check to see if the jqXHR
value is what we're expecting. One way is to check if jqXHR.responseText === 'undefined'
. If this is undefined, this is not the error we intended to handle. To not handle it, and make that error uncaught, just throw jqXHR
:
// ... lines 1 - 2 | |
(function(window, $, Routing) { | |
// ... lines 4 - 26 | |
$.extend(window.RepLogApp.prototype, { | |
// ... lines 28 - 77 | |
handleNewFormSubmit: function(e) { | |
// ... lines 79 - 86 | |
this._saveRepLog(formData) | |
.then(function(data) { | |
// ... lines 89 - 90 | |
}).catch(function(jqXHR) { | |
if (typeof jqXHR.responseText === 'undefined') { | |
throw jqXHR; | |
} | |
// ... lines 95 - 98 | |
}); | |
}, | |
// ... lines 101 - 150 | |
}); | |
// ... lines 152 - 169 | |
})(window, jQuery, Routing); |
Now, if you wanted to, you could add another .catch()
on the bottom, and inside its function, log the e
value:
// ... lines 1 - 2 | |
(function(window, $, Routing) { | |
// ... lines 4 - 26 | |
$.extend(window.RepLogApp.prototype, { | |
// ... lines 28 - 77 | |
handleNewFormSubmit: function(e) { | |
// ... lines 79 - 86 | |
this._saveRepLog(formData) | |
.then(function(data) { | |
// ... lines 89 - 90 | |
}).catch(function(jqXHR) { | |
if (typeof jqXHR.responseText === 'undefined') { | |
throw jqXHR; | |
} | |
// ... lines 95 - 96 | |
}).catch(function(e) { | |
console.log(e); | |
}); | |
}, | |
// ... lines 101 - 150 | |
}); | |
// ... lines 152 - 169 | |
})(window, jQuery, Routing); |
You see, because the first catch
throws the error, the second one will catch it.
And when we try it now, the error prints two times - jQuery's Promise logs a warning each time an error is thrown inside a Promise. And then at the bottom, there's our log.
Let's remove the second .catch()
and the if
statement:
// ... lines 1 - 2 | |
(function(window, $, Routing) { | |
// ... lines 4 - 26 | |
$.extend(window.RepLogApp.prototype, { | |
// ... lines 28 - 77 | |
handleNewFormSubmit: function(e) { | |
// ... lines 79 - 86 | |
this._saveRepLog(formData) | |
.then(function(data) { | |
// ... lines 89 - 90 | |
}).catch(function(jqXHR) { | |
var errorData = JSON.parse(jqXHR.responseText); | |
self._mapErrorsToForm(errorData.errors); | |
}); | |
}, | |
// ... lines 96 - 147 | |
}); | |
// ... lines 149 - 166 | |
})(window, jQuery, Routing); |
Why? Well, I'm not going to code defensively unless I'm coding against a situation that might possibly happen. In this case, it was developer error: my code just isn't written correctly for the server. Instead of trying to code around that, we just need to fix things!
We do the same thing in PHP: most of the time, we let exceptions happen... because it means we messed up!
Ok, we understand more about .catch()
, but we still need to fix this whole situation! To do that, we'll need to create our own Promise.
This would have been a nice moment to use
console.error()
instead of the regular log. I only learned in-depth about the other console methods recently, and in my opinion they should be taught more.This is still a really great course, though, and I learned a lot. So thanks!