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?