Execute Cleanup Logic in a JavaScript Promise Chain with Promise.prototype.finally()

Marius Schulz
InstructorMarius Schulz

Share this video with your friends

Send Tweet
Published 4 years ago
Updated 2 years ago

The Promise.prototype.finally() method lets you attach a callback to a JavaScript promise that is executed once that promise is settled, whether fulfilled or rejected. It is typically used to perform cleanup logic (such as hiding loading spinners, freeing used resources, …). By using finally(), you don't have to duplicate that cleanup logic in both the promise’s fulfillment and rejection handlers.

Instructor: [00:00] Here is the app that we built in the previous lessons. When I refresh the page, we see that where it loading and the list of Star Wars films. Let's go ahead and polish this a bit more by showing a proper spinner.

[00:13] I'm going to get rid of the innerText assignment here. Then, I'm going to jump over to our HTML file. In here, I'm going to add another div with an ID of spinner and within the div, I'm going to render the spinner gif.

[00:32] Back in our JavaScript file, I'm going to grab a reference to this spinner. These lines turn out to be a bit of set up boilerplate code. What I want to do is, I want to add a region around them called setup. I'm also going to move the getFilmTitles function in there.

[00:51] Now, I can go ahead and collapse this region and we can focus on the promise chain. All right, let's refresh the page and see the spinner in action. As we can see, the spinner is this plate and the films pop in, but we never hide the spinner.

[01:06] Let's go ahead and add another .then call here. We're going to say spinner.remove and remove the spinner from the DOM. Try this again. Now, our spinner disappears. However, this approach doesn't quite work the way we want.

[01:25] If we try to load an endpoint that doesn't exists such as the movies endpoint, well, let's see what happens. We're refreshing the page and we see then our error handlers being executed, but we still see this spinner.

[01:39] The reason for this is that we're throwing an error within our fulfillment handler, the response is not OK. In this case, it is not. If we throw an error within our fulfillment handler, however, this promise is rejected, and the next fulfillment handler and the chain is not going to be executed.

[01:55] Instead, we fall through into the next onRejected handler. Let's try to fix this. We could copy this onFulfilled handler and add in onRejected handler to this then call. This way it doesn't matter whether the promise returned by the first then call is fulfilled or rejected.

[02:12] Either way, we're going to remove this spinner. Let's make sure this is actually the case and refresh the page once more. Yep, we can see that the spinner is gone, but we can also see that we're not executing our onRejected handler anymore.

[02:27] We don't see this smiley face which means this code didn't run. This is because we're registering an onRejected handler here. We're handling the rejected promise that we return from the first then call, but within this handler, we're not throwing and we're not returning a rejected promise.

[02:44] This means that we're recovering from the previously rejected promise, and we now have a fulfilled promise. The promise that is returned by this then call is fulfilled. That means, the following catch call doesn't trigger the onRejected handler.

[02:59] This approach doesn't work. Let's try something else. What if we were to move this then call to the end of the promise chain? This way, we're going to execute our onRejected callback if we have a rejected promise, but whether or not we do, we're going to remove the spinner afterwards.

[03:17] Let's see this version in action. We're going to refresh the page. We're going to see the spinner and our sad smiley face. Perfect. Almost. What happens if within our onRejected handler something causes an error to be thrown?

[03:34] Let's simulate that by adding a throw statement here. Now, we're back to the original problem. We're executing our onRejected callback, but we're not removing the spinner anymore. We can fix this problem by once more reusing the onFulfilled handler as the onRejected handler.

[03:56] Now, it doesn't matter whether or not we got an unexpected error in the catch method. Either way, we're definitely going to remove the spinner afterwards. We finally found an approach that works, but the downside is that we had to duplicate the logic.

[04:11] This is where the finally method comes into play. Instead of saying .then, we can call .finally. Finally takes a single handler that is going to be executed once the promise is settled, meaning fulfilled or rejected. This is exactly what we want.

[04:28] Note that the finally method was only introduced as part of ECMAScript 2018. You might need to polyfill if you need to support all the JavaScript engines. Let's go through our various test cases again to make sure this behavior is correct.

[04:42] First test case, we're just throwing the error in our onRejected handler. We see this spinner and we see the error message. Perfect. Let's remove the unexpected error and refresh again. Spinner, sad face that looks good. Finally, that's access the correct endpoint.

[05:04] One more time, spinner and we get the data. I want to point out one more detail of the finally method, and that is that it is transparent. Let's say we want to return the films from this fulfillment handler.

[05:18] Even though the handler of the finally method doesn't accept any parameters and doesn't return anything, we still have access to the films if we tack on another then call and specify a fulfillment handler.

[05:32] Let's open the DevTools and see that we have access to the films. Indeed, we do. Now, this is the success case. If we access an endpoint that doesn't exist, what we'll get is the value undefined. That is because our onRejeted handler in the catch method doesn't return anything. We implicitly return undefined.

[05:54] We could explicitly return empty array here. In that case, you'd see the empty array and log to the console. To sum up, the finally method is typically used to execute some sort of clean up logic such as releasing resources, or like in this case, hiding spinners.

~ 4 years ago

:( === "frowny face" ... :)

Philip John
Philip John
~ 4 years ago

why in codesandbox you have return films.slice() but not in the videos?

Marius Schulz
Marius Schulzinstructor
~ 4 years ago

@Philip: Good catch! Unfortunately, I only realized that I should've added .slice() after recording most of the course, which is why it's in the code example, but not in the videos.

So why do we need .slice() at all? The getFilmTitles() function should be a pure function, which means it shouldn't mutate its parameters. However calling .sort() on an array will sort the array in-place rather than returning a new array in which all elements are sorted. By calling .slice() first, we create a new array which is then mutated in place. This is fine because we're no longer changing the films parameter.

This is not an issue in the code shown in this lesson because we're sorting the films array that we just got back from response.json() — nobody else is referencing it. Still, the getFilmTitles method was intended to be a pure function, so I've fixed it in the code examples.