Node callback hell + async advice
I'm new to node and have been reading a ton of lately on how best to avoid backwards hell. Thanks to my newbie ability, I put together several pyramids in my code, so I hit google to see how to avoid it.
I am creating an app using the Salesforce Marketing Cloud API ...
Here's my scenario.
- Check if a specific folder exists in my account.
- If it exists, return the folder id
- If it doesn't exist find the parent folder and get the id
- Create a folder and use the parent folder id as a property.
So, in old school code this is pretty straight forward, just follow steps 1-4 in order.
In node I got a callback function with a callback function.
So, thanks to google, I came across an async library, but not really sure how to best use it.
Here is part of my code, this is part of a larger module that I call from the main.js file that starts the server
MyHelper.prototype.test = function(name, req, callback) {
async.series([
function(next){
etHelper.folder_find("dataextension", name, next);
},function(next){
etHelper.folder_find("dataextension", "Data Extensions", next);
}
],
function(err, results){
console.log(results)
});
};
Should I use the Async waterfall method? If so, how will I check the id from step 1 and if it is not null exit
Not sure if this helps, but here is the answer I created from my ETHelper methods
[ { Success: false,
Status: 'Warning',
Message: 'Folder Not Found',
Type: 'dataextension',
Name: '10513542_SQLHelper_Test',
ID: null },
{ Success: true,
Status: 'OK',
Message: 'Folder Found',
Type: 'dataextension',
Name: 'Data Extensions',
ID: '1040721' } ]
I'm really trying to learn here, but I'm a little overwhelmed with information that I came across. There seem to be 100 different ways to do something, and it's very difficult to know which one to choose.
And finally, I don't want this message to be deleted, but here is the original method I came across to deal with it, this is a mess I know.
SQLHelper.prototype.checkInstall = function(name, req, callback) {
var response = new Object();
var deDone = false;
var qDone = false;
/*
*********************************************************************************************************
*
* This function will look for the SQLHelper folders. If they are not found they will be added
*
*********************************************************************************************************
*/
//Setup the DEFolder Lookup Filter
var deFolderParms = {
objectType: "DataFolder",
props: ["Name", "ID", "ContentType"],
filter: {
leftOperand: {
leftOperand: 'Name',
operator: 'equals',
rightOperand: name
},
operator: 'AND',
rightOperand:
{
leftOperand: 'ContentType',
operator: 'equals',
rightOperand: "dataextension"
}
}
};
//Look for the Folder
etHelper.getByName(deFolderParms, SoapClient, function(err, deFolderResponse){
//console.log(deFolderResponse);
if(deFolderResponse.length <=0)
{
//Folder Does not exist so create it
//But first we need the Parent Folder
var deParentFolderParms = {
objectType: "DataFolder",
props: ["Name", "ID", "ContentType"],
filter: {
leftOperand: {
leftOperand: 'Name',
operator: 'equals',
rightOperand: "Data Extensions"
},
operator: 'AND',
rightOperand:
{
leftOperand: 'ContentType',
operator: 'equals',
rightOperand: "dataextension"
}
}
};
etHelper.getByName(deParentFolderParms, SoapClient, function(err, deParentFolderResponse){
//console.log(deParentFolderResponse);
if(deParentFolderResponse.length > 0){
var newFolderParms = {
objectType: "DataFolder",
props: {
Name: name,
CustomerKey: name,
Description: "This folder is only to be used by the SQL Helper Application",
ContentType: "dataextension",
IsActive: true,
IsEditable: true,
AllowChildren: false,
ParentFolder:{
ID: deParentFolderResponse[0].ID
},
Client:{
ID: req.session.fuel.mid
}
},
options: {}
};
etHelper.create(newFolderParms, SoapClient, function(err, newFolderResponse){
//console.log(newFolderResponse);
if(newFolderResponse.length > 0){
deDone = true;
response.DEFolderID = newFolderResponse[0].NewID;
response.Status = true;
if(qDone)
{
callback(response);
}
}
else{
response.Status = false;
callback(response);
}
});
}
else
{
response.Status = false;
callback(response);
}
});
}
else
{
deDone = true;
response.DEFolderID = deFolderResponse[0].ID;
response.Status = true;
if(qDone)
{
callback(response);
}
}
});
//Setup the DEFolder Lookup Filter
var qFolderParms = {
objectType: "DataFolder",
props: ["Name", "ID", "ContentType"],
filter: {
leftOperand: {
leftOperand: 'Name',
operator: 'equals',
rightOperand: name
},
operator: 'AND',
rightOperand:
{
leftOperand: 'ContentType',
operator: 'equals',
rightOperand: "queryactivity"
}
}
};
//Look for the Folder
etHelper.getByName(qFolderParms, SoapClient, function(err, qFolderResponse){
//console.log(qFolderResponse);
if(qFolderResponse.length <=0)
{
//Folder Does not exist so create it
//But first we need the Parent Folder
var qParentFolderParms = {
objectType: "DataFolder",
props: ["Name", "ID", "ContentType"],
filter: {
leftOperand: {
leftOperand: 'Name',
operator: 'equals',
rightOperand: "Query"
},
operator: 'AND',
rightOperand:
{
leftOperand: 'ContentType',
operator: 'equals',
rightOperand: "queryactivity"
}
}
};
etHelper.getByName(qParentFolderParms, SoapClient, function(err, qParentFolderResponse){
//console.log(qParentFolderResponse);
if(qParentFolderResponse.length > 0){
var qnewFolderParms = {
objectType: "DataFolder",
props: {
Name: name,
CustomerKey: name,
Description: "This folder is only to be used by the SQL Helper Application",
ContentType: "queryactivity",
IsActive: true,
IsEditable: true,
AllowChildren: false,
ParentFolder:{
ID: qParentFolderResponse[0].ID
},
Client:{
ID: req.session.fuel.mid
}
},
options: {}
};
etHelper.create(qnewFolderParms, SoapClient, function(err, qnewFolderResponse){
//console.log(qnewFolderResponse);
if(qnewFolderResponse.length > 0){
qDone = true;
response.QueryFolderID = qnewFolderResponse[0].NewID;
response.Status = true;
if(deDone)
{
callback(response);
}
}
else{
response.Status = false;
callback(response);
}
});
}
else
{
response.Status = false;
callback(response);
}
});
}
else
{
qDone = true;
response.QueryFolderID = qFolderResponse[0].ID;
response.Status = true;
if(deDone)
{
callback(response);
}
}
});
};
source to share
Yes, there are at least 100 different ways to solve this problem, and many of them are not necessarily better than others - it depends on what you and your team are most comfortable with and what works best for your needs. You can really only figure it out with experience, and the same solution will not be the best for you every time.
I suggest reading callbackhell.com, a manifest on mitigating hell / pyramid doom callbacks with continue passing without external library, eg async
.
In your case, using async.waterfall
will work. It is similar to async.series
, except that it passes arguments to the next function in the series without error.
async.waterfall([
function(next){
etHelper.folder_find("dataextension", name, next);
},function(result, next){
if (null == result.ID) {
// stop everything
return next("some error");
}
etHelper.folder_find("dataextension", "Data Extensions", next);
}
], function (err, results) {
// handle `err` here if it not null
});
Also be aware of Promises , which are another construct for handling synchronization. There are several node promise libraries such as bluebird
.
Your previous code would work in a similar way, except that instead of folder_find
taking a callback argument, it should return a promise, which you reject internally on error.
etHelper.folder_find("dataextension", name)
.then(function (result) {
if (null == result.ID) {
throw new Error("some error");
}
return etHelper.folder_find("dataextension", "Data Extensions");
})
.then(function (result) {
console.log(result);
})
.catch(function (err) {
// handle error here
});
source to share