Understanding the JavaScript finally() Method for Promises

The JavaScript Promise object represents the eventual completion (or failure) of an asynchronous operation and its resulting value. Promises provide a structured way to handle asynchronous code, making it more manageable and readable. The finally() method is an essential part of promise handling, allowing you to execute code regardless of whether the promise is resolved or rejected. This is particularly useful for cleanup operations, such as closing connections, stopping loading indicators, or releasing resources.

What is the finally() Method?

The finally() method is called when the promise is settled, i.e., either resolved or rejected. It allows you to run the same code after the promise has been handled, irrespective of the outcome. It’s a way to ensure that certain actions are always performed, providing a consistent and reliable way to manage asynchronous operations.

Purpose of the finally() Method

The primary purposes of the finally() method are:

  • Resource Cleanup: Ensures resources like database connections or file handles are closed, regardless of success or failure.
  • UI Updates: Hides loading indicators or performs final UI adjustments after an asynchronous task completes.
  • Consistent Execution: Guarantees that certain code is always executed, maintaining application integrity.

Syntax of the finally() Method

The syntax for using the finally() method is straightforward:

promise.finally(callback);

Here, promise is the Promise object, and callback is the function to be executed when the promise settles. The callback function receives no arguments since the purpose of finally() is not to inspect the promise’s value or reason for rejection.

Parameters

Parameter Type Description
`callback` Function A function to be executed when the promise is settled. This function receives no arguments.

Return Value

The finally() method returns a new Promise that resolves to the same value as the original promise if it was fulfilled, or rejects with the same reason if it was rejected. The return value of the callback function within finally() is ignored.

Examples of the finally() Method

Let’s explore various examples to understand how the finally() method can be used effectively.

Basic Usage

const promise_basic = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("Promise resolved!");
  }, 1000);
});

promise_basic
  .then((result) => {
    console.log("Then:", result);
  })
  .catch((error) => {
    console.error("Catch:", error);
  })
  .finally(() => {
    console.log("Finally: Promise settled!");
  });

Output:

Then: Promise resolved!
Finally: Promise settled!

Handling Rejection

const promise_rejection = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject("Promise rejected!");
  }, 1000);
});

promise_rejection
  .then((result) => {
    console.log("Then:", result);
  })
  .catch((error) => {
    console.error("Catch:", error);
  })
  .finally(() => {
    console.log("Finally: Promise settled!");
  });

Output:

Catch: Promise rejected!
Finally: Promise settled!

Cleanup Operations: Hiding a Loading Indicator

One of the most common use cases for finally() is to perform cleanup operations. Consider a scenario where you need to display a loading indicator while fetching data and hide it once the data is loaded or an error occurs.

<div id="loadingIndicator">Loading...</div>
<button id="fetchButton">Fetch Data</button>

<script>
  const loadingIndicatorElement = document.getElementById("loadingIndicator");
  const fetchButtonElement = document.getElementById("fetchButton");

  function fetchData() {
    loadingIndicatorElement.style.display = "block";
    fetchButtonElement.disabled = true;

    return new Promise((resolve, reject) => {
      setTimeout(() => {
        const success = Math.random() > 0.5;
        if (success) {
          resolve("Data fetched successfully!");
        } else {
          reject("Failed to fetch data!");
        }
      }, 1500);
    })
      .then((result) => {
        console.log("Then:", result);
      })
      .catch((error) => {
        console.error("Catch:", error);
      })
      .finally(() => {
        loadingIndicatorElement.style.display = "none";
        fetchButtonElement.disabled = false;
        console.log("Finally: Operation completed!");
      });
  }

  fetchButtonElement.addEventListener("click", fetchData);
</script>

In this example, the loading indicator is displayed when the fetch operation starts and hidden when the operation completes, regardless of whether the data was fetched successfully or an error occurred.

Resource Management: Closing Connections

Another common use case is managing resources, such as closing database connections or file handles.

function connectToDatabase() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const connected = Math.random() > 0.2;
      if (connected) {
        console.log("Database connected");
        resolve({ close: () => console.log("Database connection closed") });
      } else {
        reject("Failed to connect to the database");
      }
    }, 500);
  });
}

connectToDatabase()
  .then((connection) => {
    console.log("Then: Performing database operations...");
    return connection;
  })
  .catch((error) => {
    console.error("Catch:", error);
  })
  .finally((connection) => {
    if (connection && connection.close) {
      connection.close();
    }
    console.log("Finally: Cleanup completed!");
  });

In this example, the finally() method ensures that the database connection is closed, even if an error occurs during the connection or subsequent operations.

Advanced Techniques

Chaining finally()

You can chain multiple finally() methods, although this is less common. Each finally() will be executed in order, regardless of the promise’s outcome.

const promise_chaining = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("Promise resolved!");
  }, 1000);
});

promise_chaining
  .then((result) => {
    console.log("Then:", result);
  })
  .catch((error) => {
    console.error("Catch:", error);
  })
  .finally(() => {
    console.log("Finally 1: First cleanup task completed!");
  })
  .finally(() => {
    console.log("Finally 2: Second cleanup task completed!");
  });

Output:

Then: Promise resolved!
Finally 1: First cleanup task completed!
Finally 2: Second cleanup task completed!

Combining finally() with Other Promise Methods

The finally() method is often used in conjunction with then() and catch() to handle both successful and failed promise outcomes.

const promise_combine = new Promise((resolve, reject) => {
  setTimeout(() => {
    const success = Math.random() > 0.5;
    if (success) {
      resolve("Operation successful!");
    } else {
      reject("Operation failed!");
    }
  }, 1000);
});

promise_combine
  .then((result) => {
    console.log("Then:", result);
  })
  .catch((error) => {
    console.error("Catch:", error);
  })
  .finally(() => {
    console.log("Finally: Operation completed, cleaning up...");
  });

Common Pitfalls

Ignoring Arguments

The finally() method does not receive any arguments. Attempting to access the promise’s value or rejection reason within finally() will result in undefined.

const promise_no_args = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("Promise resolved!");
  }, 1000);
});

promise_no_args
  .then((result) => {
    console.log("Then:", result);
  })
  .catch((error) => {
    console.error("Catch:", error);
  })
  .finally((value) => {
    console.log("Finally: Value received:", value); // value will be undefined
  });

Output:

Then: Promise resolved!
Finally: Value received: undefined

Modifying the Promise Result

The finally() method should not be used to modify the promise’s result. Any return value from the finally() callback is ignored.

const promise_no_modify = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("Original value");
  }, 1000);
});

promise_no_modify
  .then((result) => {
    console.log("Then:", result);
  })
  .catch((error) => {
    console.error("Catch:", error);
  })
  .finally(() => {
    return "Modified value"; // This return value is ignored
  })
  .then((result) => {
    console.log("Then after finally:", result); // result will be "Original value"
  });

Output:

Then: Original value
Then after finally: Original value

Real-World Applications

Implementing a Progress Indicator

Consider a scenario where you need to display a progress indicator during a file upload and hide it once the upload completes or fails.

<div id="progressIndicator">Uploading...</div>
<button id="uploadButton">Upload File</button>

<script>
  const progressIndicatorElement = document.getElementById("progressIndicator");
  const uploadButtonElement = document.getElementById("uploadButton");

  function uploadFile() {
    progressIndicatorElement.style.display = "block";
    uploadButtonElement.disabled = true;

    return new Promise((resolve, reject) => {
      setTimeout(() => {
        const success = Math.random() > 0.5;
        if (success) {
          resolve("File uploaded successfully!");
        } else {
          reject("Failed to upload file!");
        }
      }, 1500);
    })
      .then((result) => {
        console.log("Then:", result);
      })
      .catch((error) => {
        console.error("Catch:", error);
      })
      .finally(() => {
        progressIndicatorElement.style.display = "none";
        uploadButtonElement.disabled = false;
        console.log("Finally: Upload process completed!");
      });
  }

  uploadButtonElement.addEventListener("click", uploadFile);
</script>

Cleaning Up Resources in Asynchronous Operations

In asynchronous operations, it’s crucial to clean up resources to prevent memory leaks or other issues.

function performAsyncOperation() {
  let resource = null;

  return new Promise((resolve, reject) => {
    setTimeout(() => {
      try {
        resource = { id: Math.random(), release: () => console.log("Resource released") };
        console.log("Resource acquired:", resource.id);
        const success = Math.random() > 0.3;
        if (success) {
          resolve("Operation completed successfully!");
        } else {
          reject("Operation failed!");
        }
      } catch (error) {
        reject(error);
      }
    }, 1000);
  })
    .then((result) => {
      console.log("Then:", result);
    })
    .catch((error) => {
      console.error("Catch:", error);
    })
    .finally(() => {
      if (resource) {
        resource.release();
      }
      console.log("Finally: Resource cleanup completed!");
    });
}

performAsyncOperation();

Browser Support

The finally() method is widely supported across modern browsers. Here’s a summary of browser compatibility:

  • Chrome: 63+
  • Edge: 79+
  • Firefox: 58+
  • Safari: 11.1+
  • Opera: 50+

Conclusion

The finally() method is a valuable tool for ensuring that cleanup operations and consistent execution are performed in asynchronous JavaScript code. By using finally(), you can create more robust and maintainable applications that handle resources and UI updates reliably, regardless of whether a promise resolves or rejects. This ensures a smoother user experience and prevents potential issues caused by unhandled resources.