Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

All About Promises!

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

Ok, let's talk promises: JavaScript promises. These are a hugely important concept in modern JavaScript, and if you haven't seen them yet, you will soon.

We all know that in JavaScript, a lot of things can happen asynchronously. For example, Ajax calls happen asynchronously and even fading out an element happens asynchronously: we call the fadeOut() function, but it doesn't finish until later. This is so common that JavaScript has created an interface to standardize how this is handled. If you understand how it works, you will have a huge advantage.

Hello Promise

Google for "JavaScript promise" and click into the Mozilla.org article. To handle asynchronous operations, JavaScript has an object called a Promise. Yep, it's literally an object in plain, normal JavaScript - there are no libraries being used. There are some browser compatibility issues, especially with Internet Explorer... like always... but it's easy to fix, and we'll talk about it later.

This article describes the two sides to a Promise. First, if you need to execute some asynchronous code and then notify someone later, then you will create a Promise object. That's basically what jQuery does internally when we tell it to execute an AJAX call. This isn't very common to do in our code, but we'll see an example later.

The second side is what we do all the time: this is when someone else is doing the asynchronous work for us, and we need to do something when it finishes. We're already doing stuff like this in at least 5 places in our code!

Promises Versus $.ajax

Whenever something asynchronous happen, there are two possible outcomes: either the asynchronous call finished successfully, or it failed. In Promise language, we say that the Promise was fulfilled or the Promise was rejected.

Here's the basic idea: if something happens asynchronously - like an AJAX call - that code should return a Promise object. If it does, we can call .then() on it, and pass it the function that should be executed when the operation finishes successfully.

Now that we know that, Google for "jQuery Ajax" to find the $.ajax() documentation. Check this out: normally when we call $.ajax(), we don't think about what this function returns. In fact, we're not assigning it to anything in our code.

But apparently, it returns something called a jqXHR object. Search for jqXHR object on this page - you'll find a header that talks about it. First, it gives a bunch of basic details about this object. Fine. But look below the code block:

The jqXHR object implements the Promise interface, giving it all the properties, methods, and behavior of a Promise.

Woh! In other words, what we get back from $.ajax() is an object that has all the functionality of a Promise! An easy, and mostly-accurate way of thinking about this is: the jqXHR object is a sub-class of Promise.

Below, it shows you all of the different methods you can call on the jqXHR object. You can call .done(), which is an alternative to the success option, or .fail() as an alternative to the failure option. AND, check this out, you can call .then(), because .then() exists on the Promise object.

Adding Promise Handlers

In practice, this means we can call .done() on our $.ajax(). It'll receive the same data argument that's passed to success. Add a little console.log('I am successful!'). Let's also console.log(data):

... lines 1 - 2
(function(window, $, Routing) {
... lines 4 - 26
$.extend(window.RepLogApp.prototype, {
... lines 28 - 79
handleNewFormSubmit: function(e) {
... lines 81 - 100
}).done(function(data) {
console.log('I am successful!');
console.log(data);
... lines 104 - 106
})
},
... lines 109 - 150
});
... lines 152 - 169
})(window, jQuery, Routing);

And guess what? We can just chain more handlers off of this one: add another .done() that looks the same. Print a message - another handler - and also console.log(data) again:

... lines 1 - 2
(function(window, $, Routing) {
... lines 4 - 26
$.extend(window.RepLogApp.prototype, {
... lines 28 - 79
handleNewFormSubmit: function(e) {
... lines 81 - 100
}).done(function(data) {
console.log('I am successful!');
console.log(data);
}).done(function(data) {
console.log('another handler!');
console.log(data);
})
},
... lines 109 - 150
});
... lines 152 - 169
})(window, jQuery, Routing);

Using the Standard: only .then()

Effectively $.ajax() returns an object that has all the functionality of a Promise plus a few additional methods. The only methods that a true Promise has on it are .then() and .catch(), for when a promise is rejected, or fails. But jQuery's object also has .always(), .fail(), .done() and others that you can see inside what they call their "deferred object".

The story here is that jQuery implemented this functionality before the Promise object was a standard. You could use any of these methods, but instead, I want to focus on treating what we get back from jQuery as a pure Promise object. I want to pretend that these other methods don't exist, and only rely on .then() and .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) {
console.log('I am successful!');
console.log(data);
}).then(function(data) {
console.log('another handler!');
console.log(data);
})
},
... lines 109 - 150
});
... lines 152 - 169
})(window, jQuery, Routing);

In other words, I'm saying:

Don't rely on .done(), just use .then(), which is the method you would use with any other library that implements Promises.

Modifying the Value in .then

Ok, go back and refresh now. When we submit, both handlers are still called. But woh! Check this out: our first data prints out correctly... but the second one is undefined?

If you look back at the Promise documentation, this makes sense. It says:

.then() appends a fulfillment handler on the Promise and returns a new Promise resolving to the return value of the called handler.

Ah, so when we add the second .then(), that's not being attached to the original Promise, that's being attached to a new Promise that's returned from the first .then(). And according to the rules, the value for that new Promise is equal to whatever we return from the first.

Ok, so let's prove that's the case: return data:

... 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) {
console.log('I am successful!');
console.log(data);
return data;
}).then(function(data) {
console.log('another handler!');
console.log(data);
})
},
... lines 111 - 152
});
... lines 154 - 171
})(window, jQuery, Routing);

Back in the browser, it works! Both handlers are passed the same data.

But what about handling failures? Oh, that's pretty crazy.

Leave a comment!

0
Login or Register to join the conversation
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