Error handling with Node.js, Async and Formidable
Asked Answered
B

3

14

In the following snippet I would like to validate the fields in the first async method.

If they are not valid I would like to return an error to the user immediately.

How do I do that?

var form = new formidable.IncomingForm();

async1.series([
    function (callback) {
        form.parse(req);

        form.on('field', function (name, val) {
            // Get the fields
        });

        form.on('fileBegin', function (name, file) {
            if (file.name !== "") {
                file.path = __dirname + '/upload/' + file.name;
            }
        });
        callback();
    },
    function (callback) {
        form.on('file', function (name, file) {
            try {
                // Do something with the file using the fields retrieved from first async method
            }
            catch (err) {
                logger.info(err);
            }
        });


        callback();
    }
], function (err) {
    //the upload failed, there is nothing we can do, send a 500

    if (err === "uploadFailed") {
        return res.send(500);
    }

    if (err) {
        throw err;
    }
    return res.status(200);

});
Bethina answered 29/9, 2017 at 14:46 Comment(1)
You can return callback with an error return callback(err) immediately from the if block where you are checking the field, that callback will directly execute your callback handler function where you are sending response code.Mute
D
9

I would extract the form checking into a function:

var form = new formidable.IncomingForm();

function check(name, cb, err) {
 return new Promise((res,rej) => {
  form.on('field', function(n, val) {
        if(n !== name) return;
        if(cb(val)){
          res(val);
        }else{
          rej(err);
       }
   });
 });
}

form.parse(req);

So now we can implement the checks and use Promise.all to summarize them:

 Promise.all(
   check("username", val => val.length > 4, "username isnt valid"),
   check("password", val => true, "we need a password")
 ).then(_ => res.json({status:200}))
  .catch(err => res.json({err}));

If not all all parameters have been passed, this will wait endlessly. So lets terminate if it was ended:

const ended = new Promise((_,rej) => form.on("end", () => rej("params required"));

Promise.race(
 ended,
  Promise.all(
   check("username", val => val.length > 4, "username isnt valid"),
   check("password", val => true, "we need a password")
  )
).then(_ => res.json({status:200}))
 .catch(err => res.json({err}));

So with that we can create a good flow of data. e.g.:

const login = Promise.all(
  //usable as one liners
 check("username", val => val.length >= 8, "username invalid"),
 //or more extensible
 check("password", val => {
   if( val.length < 8 ) return false;
   //other checks
   console.log(password);
   return true;
 }, "password invalid")
//the field values are resolved by the promises so we can summarize them below 
).then(([username,password]) =>
   //a random (maybe async) call to evaluate the credentials
  checkAgainstDB(username,password)
  //we can directly fail here, err is  "password invalid" or "username invalid"
).catch(err => res.json({error:"login failed",details:err}));

 //another parameter can be extra handled    
const data = check("something", val => val.length);

//we need to summarize all the possible paths (login /data in this case) to one that generates the result
Promise.race(
 //here we join them together
 Promise.all(login, data)
   .then((l, d) => res.json(whatever),
 //and we use the ended promise ( from above ) to end the whole thing
 ended
  //and at last the errors that can occur if the response ended or that have not been canceled early
).catch(e => res.json(e));
Dyspnea answered 5/10, 2017 at 11:51 Comment(0)
G
3

var form = new formidable.IncomingForm();

async1.series([
    function (callback) {
        form.parse(req);

        form.on('field', function (name, val) {
            if (!name || !val) {
              // the moment callback is called with an error, async will stop execution of any of the steps
              // in the series and execute the function provided as the last argument
              // idimoatic node, when calling the callback with instance of Error
              return callback(new Error('InvalidParams'));
            }

            /**
             * This is from async documentation: https://caolan.github.io/async/docs.html#series
             * Run the functions in the tasks collection in series, each one running once the previous function 
             * has completed. If any functions in the series pass an error to its callback, no more functions are 
             * run, and callback is immediately called with the value of the error. Otherwise, callback receives 
             * an array of results when tasks have completed.
             */
        });

        form.on('fileBegin', function (name, file) {
            if (file.name !== "") {
                file.path = __dirname + '/upload/' + file.name;
            }
        });

        form.on('end', function () {
          // call callback with null to specify there's no error
          // if there are some results, call it like callback(null, results);
          return callback(null);
        });

        // if you call the callback immediately after registering event handlers for on('field') etc,
        // there will be no time for those events to be triggered, by that time, this function will be 
        // done executing.
        //callback();
    },
    function (callback) {
        form.on('file', function (name, file) {
            try {
                // Do something with the file using the fields retrieved from first async method
            }
            catch (err) {
                logger.info(err);
                return callback(err);
            }
        });

        // This should also not be called immediately
        //callback();
    }
], function (err) {
    //the upload failed, there is nothing we can do, send a 500

    if (err === "uploadFailed") {
        return res.send(500);
    }

    if (err.message === 'InvalidParams') {
      // This will be immediately returned to the user.
      return res.sendStatus(400);
    }

    if (err) {
      // I'm not sure if this was just for the example, but if not, you should not be throwing an error
      // at run time. 
        throw err;
    }
    return res.status(200);

});

I added some comments in the code where I needed to show where and how to create an error and how it's bubbled up to the user immediately.

Reference: Async Documentation

P.S. Code Snippet is not runnable, but it has a better representation of the code.

-- edit --

After knowing more from the comment, adding another snippet. You are unreasonably mixing callback and event handling. You can just pass a callback to form.parse and the callback is called when all fiels are collected. You can do validation, return error immediately or just handle the form fields right away.

form.parse(req, function(err, fields, files) {
  if (err) return res.sendStatus(400);
  if (fields.areNotValid()) return res.sendStatus(400);
  // parse fields
});

Or, you can register event handlers for it. All events as they flow in will be handled concurrently, like async.series.

var form = new formidable.IncomingForm();

form.parse(req);
form.on('field', (name, val) => {
  if (!name || val) {
    console.log('InvalidParams')
    return res.sendStatus(400);
  }
});
form.on('fileBegin', (name, file) => {
  if (file.name !== "") {
    file.path = __dirname + '/upload/' + file.name;
  }
});
form.on('file', (name, file) => {

});
form.on('error', (err) => {
  console.log('ParsingError');
  return res.sendStatus(400);
})
form.on('end', () => {
  if (res.headersSent) {
    console.log('Response sent already')
  } else {
    // handle what you want to handle at the end of the form when all task in series are finished
    return res.sendStatus(200);
  }
});
Godewyn answered 4/10, 2017 at 5:14 Comment(2)
This will cause a callback() is already called error when we return the error. The callback in form.on('end') throws an errorBethina
@Bethina Gotcha! I understand the problem better now and updated my answer. Please check the edits and let me know if that helps.Godewyn
W
1

I'm assuming that this is a good place to validate since this is when the fields are coming in:

form.on('field', function (name, val) {
    //if values are null
    if (!name || !val) {
        //pass the callback an error 
        return callback("Values are null")
    }
    // Get the fields
});

Please let me know if this helps.

Wendolynwendt answered 2/10, 2017 at 10:1 Comment(2)
Where those the error get thrown to? Is there a way to return the error to the API call?Bethina
In my code, I believe it would get sent to the last function in the async series through the error parameter. To send it to the API call (I'm assuming something like Express), maybe you could do 'return res.send('Values are null')Wendolynwendt

© 2022 - 2024 — McMap. All rights reserved.