Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Error handling with Node.js, Async and Formidable

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);

});
like image 878
Robben_Ford_Fan_boy Avatar asked Sep 29 '17 14:09

Robben_Ford_Fan_boy


People also ask

Should you always use try catch with async await?

No. You don't need to use try/catch in every async/await. You only need to do it at the top level.

How do I return async function error?

To recap: Throwing error from an async function won't spit out a "plain exception". Async functions and async methods always return a Promise, either resolved or rejected. To intercept exceptions from async functions you must use catch() .


2 Answers

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));
like image 61
Jonas Wilms Avatar answered Oct 29 '22 15:10

Jonas Wilms


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);
  }
});
like image 45
Neeraj Sharma Avatar answered Oct 29 '22 16:10

Neeraj Sharma