Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

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.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Yay! 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.

Leave a comment!

2
Login or Register to join the conversation

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!

Reply

Hey Mathieu

We are glad to hear that you are liking our tutorials :)
About console.error() you're technically correct, that's the right function for printing errors in console, but in this case, we only want to see the error message and then remove the logging.

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial uses an older version of Symfony... but since it's a JavaScript tutorial, the concepts are still ? valid!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.2.0",
        "symfony/symfony": "3.1.*", // v3.1.10
        "twig/twig": "2.10.*", // v2.10.0
        "doctrine/orm": "^2.5", // v2.7.1
        "doctrine/doctrine-bundle": "^1.6", // 1.10.3
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.2
        "symfony/swiftmailer-bundle": "^2.3", // v2.4.0
        "symfony/monolog-bundle": "^2.8", // 2.12.0
        "symfony/polyfill-apcu": "^1.0", // v1.2.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.16
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "friendsofsymfony/user-bundle": "~2.0@dev", // dev-master
        "doctrine/doctrine-fixtures-bundle": "~2.3", // v2.4.1
        "doctrine/doctrine-migrations-bundle": "^1.2", // v1.2.1
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "friendsofsymfony/jsrouting-bundle": "^1.6" // 1.6.0
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.1.1
        "symfony/phpunit-bridge": "^3.0" // v3.1.6
    }
}
userVoice