3

I'm trying to assert an error which is thrown async in an express middleware:

The middleware to test:

const request = require('request');
const middleware = function (options) {
  if (!options) {
    throw new Error('Options are missing.'); // gets catched
  }

  request(options.uri, (err, response) => {
    if(err) {
      throw err;
    }
  });

  return function (req, res, next) {}
}

module.exports = middleware;

The mocha test:

describe('middleware', () => {
  describe('if async error is thrown', () => {
    it('should return an error', done => {
      try {
        middleware({
          uri: 'http://unkown'
        });
      } catch (err) {
        assert.equal('Error: getaddrinfo ENOTFOUND unkown unkown:80', err.toString());

        return done();
      }
    });
  });
})

The problem is, that err doesn't get catched inside the test:

Uncaught Error: getaddrinfo ENOTFOUND unkown unkown:80
      at errnoException (dns.js:27:10)
      at GetAddrInfoReqWrap.onlookup [as oncomplete] (dns.js:78:26)

I understand that it happens because the error is thrown async but I don't know how to work around it here.

Alexander Zeitler
  • 11,919
  • 11
  • 81
  • 124

3 Answers3

2

The basic problem is that you throw from within asynchronous code. To cut it short: Don't do that, it's bad ;-)

So, IMHO you have a few options. Which one is most appropriate for you depends on your use case.

Scenario 1: Asynchronous setup function

Turn your synchronous into an asynchronous setup function, i.e., do not return the middleware function, but hand it over to the caller using a callback. This way the code for the caller gets worse, but you have a clean synchronous / asynchronous code split and don't mix paradigms that aren't meant for being mixed.

const middleware = function (options, callback) {
  if (!options) {
    throw new Error('Options are missing.');
  }

  request(options.uri, (error, response, body) => {
    if (error) {
      return callback(error);
    }

    callback(null, function (req, res, next) {
      // ...

      next();
    });
  });
};

Scenario 2: Do the request on each call to the middleware

Instead of doing the call to request once, do it everytime the middleware runs. I don't know whether this makes sense, but this way you can always return synchronously, and only have to deal with being asynchronous in the middleware itself.

const middleware = function (options, callback) {
  if (!options) {
    throw new Error('Options are missing.');
  }

  return function (req, res, next) {
    request(options.uri, (error, response, body) => {
      if (error) {
        return next(error);
      }

      // ...

      next();
    });
  });
};

Scenario 3: Outsource the request

The third, and IMHO the best, option is to outsource the request, and hand over the result to the middleware's setup function, instead of letting the setup function doing the request.

This not only solves your synchronous vs asynchronous problem, this also makes things easier to test, as you do not depend on an HTTP call, but instead can hand over the desired result manually.

Golo Roden
  • 140,679
  • 96
  • 298
  • 425
0

As you are working with asynchronous code, try-catch & throw can't help you as they deal only with synchronous code.

A workaround

const request = require('request');

const middleware = function(options) { // <--- outer scope

    var api_output = null; // Store the output of the HTTP request here

    if (!options) throw new Error('Options are missing.'); // Express shouldn't handle this error, hence we throw it (also this error is synchronous)

    return function(req, res, next) { // <--- inner scope

        if (!api_output) { // Check if the output of the HTTP request has already been saved (so as to avoid calling it repeatedly)

            request(options.uri, (error, response, body) => { // Perform the HTTP request

                if (error) return next(error); // Pass the error to express error-handler

                api_output = { // Save the output of the HTTP request in the outer scope
                    response: response,
                    body: body
                };

                req.api_output = api_output; // Set the output of the HTTP request in the req so that next middleware can access it

                next(); // Call the next middleware
            });

        } else { // If the output of the HTTP request is already saved

            req.api_output = api_output; // Set the output of the HTTP request in the req so that next middleware can access it

            next(); // Call the next middleware
        }
    };
}

module.exports = middleware;

What I've done is simply returned an express-middleware that calls the external API only if it has not already been called. If there is no error, then it stores the response of the external API in api_output and also pass it to the next express-middleware where you can use it.


To understand how this works it is essential to know how JavaScript deals with scopes (look up closure).

Instead of calling the 3rd party API every time the express-middleware gets executed, I have stored the output of that API in the outer scope of the express-middleware function the first time it is called. The advantage of doing this is that the output of the 3rd party API is private to the outer & inner scope and not accessible from anywhere else in the app.


If there is any error, it is passed to the next callback (this triggers the express error-handler functions). See "Express error handling", to see how this works. Specifically this -

If you pass anything to the next() function (except the string 'route'), Express regards the current request as being in error and will skip any remaining non-error handling routing and middleware functions.

You can use the above function in this way while creating your routes:

const router = require('express').Router();

router
  .get('/my-route', middleware({
    uri: 'my-url'
  }), function(req, res, next) {

    // here you can access req.api_output.response & req.api_output.body
    // don't forget to call next once processing is complete
  });

Now, regarding testing

In your mocha testing framework, you can call your API using request module in the same way that you have used it in your middleware function to call the external API. Then you can assert the output easily:

  1. Use try-catch with assertions to handle the Error from middleware function
  2. Use normal assertions to deal with errors from the express-middlewares

Note that I have used the term "express-middleware" to refer only to middlewares like function(req, res, next) {}. And I've used the term "middleware" to refer to your function called middleware.

Community
  • 1
  • 1
Soubhik Mondal
  • 2,666
  • 1
  • 13
  • 19
  • 2
    The suggested code performs the HTTP request for each incoming HTTP request, whereas the original code performed it only when the middleware was created, which is a different thing. – robertklep Feb 21 '17 at 11:31
  • @robertklep well said. I'll make changes accordingly – Soubhik Mondal Feb 21 '17 at 11:39
  • @robertklep I have a question. If the HTTP request has to be done only once, then why relate it to the middleware at all? I could simply make the request when the server is started and store it in the DB or some global variable.. – Soubhik Mondal Feb 21 '17 at 17:37
  • 1
    You'd have to ask the OP for the rationale for doing it from a middleware, but I agree with you that these kinds of things are better done on a global level. – robertklep Feb 21 '17 at 19:16
  • @BlazeSahlzen that's indeed a valid question. If I put the request outside the middleware I'm "owning" the bootstrapping not only of my middleware but also the app itself which not everybody might like. – Alexander Zeitler Feb 21 '17 at 19:24
  • Could you please rephrase that? I didn't quite understand – Soubhik Mondal Feb 21 '17 at 19:38
  • @AlexanderZeitler OTOH, you could own the bootstrapping more explicitly by using something like [`bootable`](https://github.com/jaredhanson/bootable). – robertklep Feb 21 '17 at 20:48
0

In your middleware function, return a promise as a result from your try-catch. so your catch block would `reject(err).

I would do something like this:

const request = require('request');
const middleware = function (options) {
    return new Promise((resolve,reject)=> {
        if (!options) {
            reject('Options are missing.'); 
        }

        request(options.uri, (err, response) => {
            if(err) {
                reject(err)
            } else {
                resolve(response)
            }
        });
    });
}

Then, for your test case, do something like:

describe('middleware', () => {
    describe('if async error is thrown', () => {
        it('should return an error', (done) => {
            middleware({uri: 'http://unkown'}).then((res)=>{
                done(res)  //this will fail because you expect to never hit this block
            },(err)=>{
                assert.equal('Error: getaddrinfo ENOTFOUND unkown unkown:80', err.toString());
                done()
            });
        });
    });
})
LostJon
  • 2,287
  • 11
  • 20
  • I've edited your answer to make it look like a standalone answer. Your initial phrasing made it look to potential readers like it was perhaps meant as a reply to a comment. If it were a reply to a comment, then it should also be a comment and the people could flag your answer for deletion. – Louis Feb 22 '17 at 15:57
  • @Louis thanks for that, the comment formatting is a bit rough when showing code blocks, so i threw it in an answer. I appreciate your edits either way and I will be more thorough in future responses. – LostJon Feb 22 '17 at 15:59
  • Thanks, tried that but it requires me to first to call `middleware(options)` and inside the resolve I can register the inner, return function (e.g. for express mw) in express. I don't want to "own" the mw-registration process in that way. – Alexander Zeitler Feb 26 '17 at 15:45
  • I want to register the mw that way: `app.use(middleware(options))`; – Alexander Zeitler Feb 26 '17 at 15:52