Tips for preventing the nesting of promises when managing errors from various APIs

Currently, I am developing an application that requires making requests to two different APIs. The authentication process is handled by Cognito, while a lambda function communicates with a database. However, the issue I am facing does not seem to be specific to these implementations alone. It could potentially occur with any pair of APIs.

My task involves creating a new user account. Firstly, I need to register a new user in Cognito for login access. Subsequently, I must also create a corresponding user entry in the database to store non-authentication related data. In case an error occurs during one of the API requests, I have to ensure that any items created in the other API are promptly deleted.

The current approach I am using can be summarized as follows:

const signUpNewUser = (userInfo) => {
  API.post("user", "/user", userInfo)
    .then((response) => {
      return COGNITO.post("user", "/user", response.newUserID);
    })
    .then((res) => {
      //Proceed if both requests are successful
    })
    .catch((error) => {
      if (error.origin === "COGNITO_ERROR") {
        //If Database changes are confirmed but Cognito fails, delete created guest in DB
        return API.delete("guest", "/guest", userInfo);
      } else if (error.origin === "DATABASE_ERROR") {
        //If Database changes fail and Cognito has not executed yet, no deletion required in this scenario
      }
    });
};

This pattern closely resembles what is commonly seen online. However, I find it challenging to differentiate between Cognito errors and database errors. While my code categorizes them based on their origin property, such distinction is not consistently reliable. Dealing with multiple APIs whose behavior is beyond my control might lead to such difficulties, and I am searching for a more effective solution.

It appears that nesting promises may provide a resolution in this situation. By introducing nested catch blocks after API.Post and COGNITO.post, errors can be intercepted, modified with an origin property, and then propagated through the promise chain for centralized error handling:

const signUpNewUser2 = (userInfo) => {
  API.post("user", "/user", userInfo)
    .catch((err) => {
      let parsedError = err;
      parsedError.origin = "DATABASE_ERROR";
      throw parsedError;
    })
    .then((response) => {
      let newGuestID = response.id;
      return COGNITO.post("user", "/user", newGuestID)
        .then((res) => {
          return res;
        })
        .catch((err) => {
          let parsedError = err;
          parsedError.origin = "COGNITO_ERROR";
          throw parsedError;
        });
    })
    .then((res) => {
      //Proceed if both requests succeed
    })
    .catch((error) => {
      if (error.origin === "COGNITO_ERROR") {
        //If DB changes confirm but Cognito fails, delete created guest in DB
        return API.delete("guest", "/guest", guestInfo);
      } else if (error.origin === "DATABASE_ERROR") {
        //If DB changes fail and Cognito has not executed yet, no deletion needed in this case
      }
    });
};

Despite conventional advice discouraging nested promises, this method seems promising for resolving the issue at hand. An alternative could involve encapsulating API.post and COGNITO.post within separate functions containing internal .then/.catch statements, ultimately returning promises or throwing errors with added properties indicating origin. Nevertheless, contention exists regarding whether such an approach complicates code readability rather than offering clarity.

The prevailing practice typically advocates for a single catch block positioned towards the end of a .then chain capable of managing diverse types of errors. Yet, when dealing with external APIs over which you have limited control, accurately identifying and handling errors becomes a fundamental challenge. Is there a fundamental aspect of JavaScript error handling that eludes me?

Answer №1

When you need to make API calls in a serial manner, managing them should be simple. Just execute COGNITO.post within a .then block after the initial API call - there's no requirement to add another .catch block in between.

const signUpNewUser2 = (userInfo) => {
    API.post("user", "/user", userInfo)
        .then((response) => {
            let newGuestID = response.id;
            return COGNITO.post("user", "/user", newGuestID)
                .then(handleBothSuccess)
                .catch((err) => {
                    // COGNITO failed
                    return API.delete("guest", "/guest", guestInfo);
                });
        })
        .then((res) => {
            //BOTH REQUESTS OCCURRED WITH NO ERRORS
        })
        .catch((error) => {
            // Some error other than COGNITO failing occurred
        });
};

If your control flow necessitates nesting Promises, there's nothing inherently wrong with it - or you could declare the .then and .catch functions as separate variables first to avoid deep nesting visually.

Another approach is to consider utilizing async/await, which might offer clearer readability.

const signUpNewUser2 = async (userInfo) => {
    let newGuestId;
    try {
        newGuestId = await API.post("user", "/user", userInfo);
    } catch (e) {
        // Handle API failure here if needed...
        return;
    }
    let cognitoResponse;
    try {
        cognitoResponse = await COGNITO.post("user", "/user", newGuestID);
    } catch (e) {
        // COGNITO failed
        // If deleting throws an error, it will propagate to the caller
        return API.delete("guest", "/guest", guestInfo);
    }
    //BOTH REQUESTS OCCURRED WITH NO ERRORS
};

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Which tools should I combine with Angular for developing both Android and iOS applications?

My primary focus has been on developing web apps using Angular, but now I am interested in creating native Android and iOS apps with Angular. I have heard about using Cordova, Capacitor, and NativeScript for this purpose, as alternatives to Ionic due to pe ...

Is it possible for me to pass a reference to a specific object's instance function?

Can JavaScript allow you to pass a function reference to a specific object's function, similar to what can be done in Java? Let's take a look at this code snippet: _.every(aS, function (value) { return exp.test(value); }); What if we want ...

Display JavaScript and CSS code within an Angular application

I am working on an Angular 1.x application (using ES5) where I need to display formatted CSS and JS code for the user to copy and paste into their own HTML file. The code should include proper indentations, line-breaks, etc. Here is a sample of the code: ...

What is the proper way to showcase the hover effect on a nested ul list item in the DOM?

In an attempt to create a menu, I have written the following code: var sheet_nav = document.createElement('style'); sheet_nav.innerHTML = "nav{ margin: 100px auto; text-align: center;}"; document.head.appendChild(sheet_nav); var sheet_nav_ul = ...

Troubleshooting a JQuery AJAX Autocomplete problem involving PHP and MySQL

I am facing an issue with my autocomplete feature. It is functioning properly on one of my pages, but not on this particular page. Even though the correct number of entries is being retrieved, they all appear to be "blank" or are displayed in black text th ...

Assign a background image to a button using an element that is already present on the page

Is there a way to set the background-image of a button without using an image URL? I am hoping to use an element already in the DOM as the background-image to avoid fetching it again when the button is clicked. For example, caching a loading gif within the ...

Execute the function that has been passed through a data attribute

I am attempting to execute a function using its name, which is passed through a data attribute in the HTML. I have made an attempt with the code below: HTML: <div class="generic-select" data-help-on-shown="genericFunction" > </div> ...

The function '.save' is not recognized by Mongoose

As a newcomer, I have been trying to understand the code in this calendar app that I created using express-generator. Everything seems to be working fine with connecting to MongoDB, but I am facing issues when trying to save a document. The section of my ...

A pop-up window for selecting a file within an <a> tag

Is there a way to create an Open File dialog box on a link within my webpage? I attempted <input name="uploadedfile" type="file"> However, it only functions as a button and does not allow for the selection of multiple files. I would like somethin ...

Struggling to populate a fresh JavaScript array with values submitted from a form

I am currently working on a form to collect Family information that will be used for enrolling in a new benefit plan. My goal is to populate an array with the necessary values for each individual to create the benefit record. While researching, I have only ...

Capture the current state of a page in Next.js

As I develop my Next.js application, I've encountered an architectural challenge. I'm looking to switch between routes while maintaining the state of each page so that I can return without losing any data. While initialProps might work for simple ...

Creating a streamlined RESTful service for Backbone.js using Apache on a Windows platform

I'm currently in the process of setting up a RESTful web service on my localhost using Apache to serve as the backend for my Backbone application. I've encountered some challenges along the way: Initially, I attempted to configure WebDAV, but r ...

Decoding various JSON arrays received through AJAX requests

After returning two objects as JSON through AJAX, I am facing an issue with accessing the values in these two lists. Previously, when I had only one list, I could parse it easily. data = serialize("json", vm_obj) data2 = serialize("json", user_networks_li ...

Having trouble with res.redirect not working after the page has been rendered with data?

I have a basic forget password feature set up, where users can request a password change and receive an email with a token. Clicking the link in the email will redirect them to a page where they can input their new password. When I click on the email link ...

Conceal Bootstrap Toast for a day following dismissal

I have implemented Bootstrap 5 toasts to showcase an advertisement on my website. The goal is to make the advertisement disappear for 24 hours once the user closes it. Here's the current code snippet: <div class="position-sticky bottom-0" ...

Looking for Sha1 encryption functionality in jQuery or JavaScript?

I am currently using sha1 encryption coding in PHP <?php echo hash('sha1', 'testing'.'abcdb'); ?> However, I need this encryption to work on a page named xyz.html, causing the current block of code to not function prop ...

Embark on the journey of incorporating the Express Router

My Nodejs server is set up with router files that use absolute routes for the HTTP methods, such as /api/users/all. // /routes/user.routes.js module.exports = (app) => { app.use((req, res, next) => { res.header( "Access-Control-All ...

How to troubleshoot syntax errors when using the delete function in Three.js within Eclipse

When using the Javascript Library Three.js, it is important to note that the delete keyword can be used as an object property. While this is valid under ECMAScript 5 standards, Eclipse currently only supports ECMA 3. As stated in the Mozilla reference, re ...

extracting array index from a Mongoose array

//index.js let countryNameList = { "name"=["Bangladesh","India","Australia"] } //Output Section let findCountryIndex = awaitDataModel.find({$indexOfArray:{CountryName:"Bangladesh"}}) console.log(findCountryIndex); //Expecting Output : 0 I am l ...

Methods to prompt a user to select an option using Bootstrap

I have been struggling with this problem for hours and it's really starting to frustrate me! Currently, I am incorporating Twitter Bootstrap along with bootstrap-min.js. This is the code snippet in question: <select id="assettype" name="assettyp ...