20

I am running external code as a 3rd party extension to a node.js service. The API methods return promises. A resolved promise means the action was carried out successfully, a failed promise means there was some problem carrying out the operation.

Now here's where I'm having trouble.

Since the 3rd party code is unknown, there could be bugs, syntax errors, type issues, any number of things that could cause node.js to throw an exception.

However, since all the code is wrapped up in promises, these thrown exceptions are actually coming back as failed promises.

I tried to put the function call within a try/catch block, but it's never triggered:

// worker process
var mod = require('./3rdparty/module.js');
try {
  mod.run().then(function (data) {
    sendToClient(true, data);
  }, function (err) {
    sendToClient(false, err);
  });
} catch (e) {
  // unrecoverable error inside of module
  // ... send signal to restart this worker process ...
});

In the above psuedo-code example, when an error is thrown it turns up in the failed promise function, and not in the catch.

From what I read, this is a feature, not an issue, with promises. However I'm having trouble wrapping my head around why you'd always want to treat exceptions and expected rejections exactly the same.

One case is about actual bugs in the code, possibly irrecoverable -- the other is just possible missing configuration information, or a parameter, or something recoverable.

Thanks for any help!

Nick Jennings
  • 3,853
  • 6
  • 30
  • 45
  • The design is that there are no expected rejections (I don't like it), the actual feature is that exceptions are reported even if thrown in asynchronous code – Bergi Oct 23 '13 at 10:10
  • @Bergi because having to restart the server is much more desirable? wat – Esailija Oct 23 '13 at 10:45
  • @Esailija: How else would unknown exceptions supposed to be handled? I would prefer to catch known exceptions by hand and trigger rejection manually, and use domains/window.onerror/process.onuncaughtexceptions for everything else. – Bergi Oct 23 '13 at 10:51
  • @Bergi by closing any opened resources and reporting an error to the user? You still have to crash the server even if you use uncaught exception because you don't know what resources were left open. – Esailija Oct 23 '13 at 10:52
  • @Esailija: That as well of course, but [it's recommended](http://www.nodejs.org/api/domain.html#domain_warning_don_t_ignore_errors) to still restart the process after that – Bergi Oct 23 '13 at 10:55
  • @Bergi Yes, but if you try-catch-finally locally (like in promises), you don't have to crash any server because you are handling the error locally right where it happened. http://spion.github.io/posts/why-i-am-switching-to-promises.html – Esailija Oct 23 '13 at 10:57
  • @Esailija: Sure I'm trying that as well where it is possible and I like to use promises for it (manually). Yet current promise design does not allow to "throw globally", this "always-local-try-catch" leads to forgotten `done()`s etc… – Bergi Oct 23 '13 at 11:00
  • @Bergi Promises should do the same as sync code, sync code will catch any error when you have try-catch, so what would you do with sync code? – Esailija Oct 23 '13 at 11:11
  • @Esailija: I don't like that comparison. Maybe promises can be more powerful than sync code? :-) – Bergi Oct 23 '13 at 11:17
  • @Bergi that would not be more powerful but surprising and random behavior, if you have try catch then you have try catch. Note that for example my promise implementation allows you to easily listen to certain kind of errors and the programmer error here is easily debuggable if you look at console http://jsfiddle.net/sU9UN/ But expecting such discrimination on error types being done at library level automatically would not go well. – Esailija Oct 23 '13 at 11:22

3 Answers3

15

Crashing and restarting a process is not a valid strategy to deal with errors, not even bugs. It would be fine in Erlang, where a process is cheap and does one isolated thing, like serving a single client. That doesn't apply in node, where a process costs orders of magnitude more and serves thousands of clients at once

Lets say that you have 200 requests per second being served by your service. If 1% of those hit a throwing path in your code, you would get 20 process shutdowns per second, roughly one every 50ms. If you have 4 cores with 1 process per core, you would lose them in 200ms. So if a process takes more than 200ms to start and prepare to serve requests (minimum cost is around 50ms for a node process that doesn't load any modules), we now have a successful total denial of service. Not to mention that users hitting an error tend to do things like e.g. repeatedly refresh the page, thereby compounding the problem.

Domains don't solve the issue because they cannot ensure that resources are not leaked.

Read more at issues #5114 and #5149.

Now you can try to be "smart" about this and have a process recycling policy of some sort based on a certain number of errors, but whatever strategy you approach it will severely change the scalability profile of node. We're talking several dozen requests per second per process, instead of several thousands.

However, promises catch all exceptions and then propagate them in a manner very similar to how synchronous exceptions propagate up the stack. Additionally, they often provide a method finally which is meant to be an equivalent of try...finally Thanks to those two features, we can encapsulate that clean-up logic by building "context-managers" (similar to with in python, using in C# or try-with-resources in Java) that always clean up resources.

Lets assume our resources are represented as objects with acquire and dispose methods, both of which return promises. No connections are being made when the function is called, we only return a resource object. This object will be handled by using later on:

function connect(url) {
  return {acquire: cb => pg.connect(url), dispose: conn => conn.dispose()}
}

We want the API to work like this:

using(connect(process.env.DATABASE_URL), async (conn) => {
  await conn.query(...);
  do other things
  return some result;
});

We can easily achieve this API:

function using(resource, fn) {
  return Promise.resolve()
    .then(() => resource.acquire())
    .then(item => 
      Promise.resolve(item).then(fn).finally(() => 
        // bail if disposing fails, for any reason (sync or async)
        Promise.resolve()
          .then(() => resource.dispose(item))
          .catch(terminate)
      )
    );
}

The resources will always be disposed of after the promise chain returned within using's fn argument completes. Even if an error was thrown within that function (e.g. from JSON.parse) or its inner .then closures (like the second JSON.parse), or if a promise in the chain was rejected (equivalent to callbacks calling with an error). This is why its so important for promises to catch errors and propagate them.

If however disposing the resource really fails, that is indeed a good reason to terminate. Its extremely likely that we've leaked a resource in this case, and its a good idea to start winding down that process. But now our chances of crashing are isolated to a much smaller part of our code - the part that actually deals with leakable resources!

Note: terminate is basically throwing out-of-band so that promises cannot catch it, e.g. process.nextTick(() => { throw e });. What implementation makes sense might depend on your setup - a nextTick based one works similar to how callbacks bail.

How about using callback based libraries? They could potentially be unsafe. Lets look at an example to see where those errors could come from and which ones could cause problems:

function unwrapped(arg1, arg2, done) {
  var resource = allocateResource();
  mayThrowError1();
  resource.doesntThrow(arg1, (err, res) => {
    mayThrowError2(arg2);
    done(err, res);
  });
}

mayThrowError2() is within an inner callback and will still crash the process if it throws, even if unwrapped is called within another promise's .then. These kinds of errors aren't caught by typical promisify wrappers and will continue to cause a process crash as per usual.

However, mayThrowError1() will be caught by the promise if called within .then, and the inner allocated resource might leak.

We can write a paranoid version of promisify that makes sure that any thrown errors are unrecoverable and crash the process:

function paranoidPromisify(fn) {
  return function(...args) {
    return new Promise((resolve, reject) =>   
      try {
        fn(...args, (err, res) => err != null ? reject(err) : resolve(res));
      } catch (e) {
        process.nextTick(() => { throw e; });
      }
    }
  }
}

Using the promisified function within another promise's .then callback now results with a process crash if unwrapped throws, falling back to the throw-crash paradigm.

Its the general hope that as you use more and more promise based libraries, they would use the context manager pattern to manage their resources and therefore you would have less need to let the process crash.

None of these solutions are bulletproof - not even crashing on thrown errors. Its very easy to accidentally write code that leaks resources despite not throwing. For example, this node style function will leak resources even though it doesn't throw:

function unwrapped(arg1, arg2, done) {
  var resource = allocateResource();
  resource.doSomething(arg1, function(err, res) {
    if (err) return done(err);
    resource.doSomethingElse(res, function(err, res) {
      resource.dispose();
      done(err, res);
    });
  });
}

Why? Because when doSomething's callback receives an error, the code forgets to dispose of the resource.

This sort of problem doesn't happen with context-managers. You cannot forget to call dispose: you don't have to, since using does it for you!

References: why I am switching to promises, context managers and transactions

Community
  • 1
  • 1
Gjorgi Kjosev
  • 1,559
  • 14
  • 24
  • Thanks for this explanation, it's starting to make more sense. However, since these 'modules' are 3rd party code, I can't ensure that they are cleaning up their resources correctly, which is the crux of the issue. Is there a way to clean out resources used in a module without knowing what they were? (they could be websocket connects, http requests or servers, any number of things could be going on in one of these modules). – Nick Jennings Oct 23 '13 at 12:31
  • Also, are there some node modules which implement this type of cleanup management? – Nick Jennings Oct 23 '13 at 12:33
  • If the module is written using promises, you shouldn't have to ensure it. If you're wrapping a (callback based) module that follows the throw-crash paradigm, and don't want promises subverting that, I suppose your wrapper can use process.nextTick to escape the promise error propagation. – Gjorgi Kjosev Oct 23 '13 at 12:54
  • Of course promises still don't capture errors within inner node-style callbacks of the library, so those will happily continue to crash the process. I should probably write an article that expands upon all this in more detail. – Gjorgi Kjosev Oct 23 '13 at 13:08
  • If the module uses promises, why shouldn't you have to ensure they've cleaned up their connections or objects? Not sure how you can make that leap, when in your example you wrote custom code to clean up your connection -- I can't be sure the module is well written. I need to be able to wipe the slate clean if I get a failure. – Nick Jennings Oct 23 '13 at 13:25
  • I make that leap because the context manager pattern is so easy to use. Of course if they're not using it, then you're out of luck. Keep in mind that node style callbacks could still be leaking resources by doing `if (err) return callback(err);` without crashing the process. Its unfortunately a best-effort solution, not a bullet-proof one. – Gjorgi Kjosev Oct 23 '13 at 13:29
  • *Crashing and restarting a process is not a valid strategy to deal with errors, not even bugs.* That's silly. There are absolutely times where you need to crash. You should load balancing in place to take care of requests after a crash. – rtf May 03 '16 at 16:30
2

It is almost the most important feature of promises. If it wasn't there, you might as well use callbacks:

var fs = require("fs");

fs.readFile("myfile.json", function(err, contents) {
    if( err ) {
        console.error("Cannot read file");
    }
    else {
        try {
            var result = JSON.parse(contents);
            console.log(result);
        }
        catch(e) {
            console.error("Invalid json");
        }
    }

});

(Before you say that JSON.parse is the only thing that throws in js, did you know that even coercing a variable to a number e.g. +a can throw a TypeError?

However, the above code can be expressed much more clearly with promises because there is just one exception channel instead of 2:

var Promise = require("bluebird");
var readFile = Promise.promisify(require("fs").readFile);

readFile("myfile.json").then(JSON.parse).then(function(result){
    console.log(result);
}).catch(SyntaxError, function(e){
    console.error("Invalid json");
}).catch(function(e){
    console.error("Cannot read file");
});

Note that catch is sugar for .then(null, fn). If you understand how the exception flow works you will see it is kinda of an anti-pattern to generally use .then(fnSuccess, fnFail).

The point is not at all to do .then(success, fail) over , function(fail, success) (I.E. it is not an alternative way to attach your callbacks) but make written code look almost the same as it would look when writing synchronous code:

try {
    var result = JSON.parse(readFileSync("myjson.json"));
    console.log(result);
}
catch(SyntaxError e) {
    console.error("Invalid json");
}
catch(Error e) {
    console.error("Cannot read file");
}

(The sync code will actually be uglier in reality because javascript doesn't have typed catches)

Esailija
  • 138,174
  • 23
  • 272
  • 326
  • So, how do you differentiate between a thrown error and a promise failure? – Nick Jennings Oct 23 '13 at 10:25
  • @NickJennings If there was a programmer error in the synchronous code like `console.log(resalt)` (a typo) what would happen with the synchronous code? – Esailija Oct 23 '13 at 10:40
  • 1
    An error would be thrown. However, with promises that results in a rejected promise. When, ideally, I would like to catch it in an uncaughException – Nick Jennings Oct 23 '13 at 13:27
  • @NickJennings An error would only be thrown if you didn't have any try catch wrapping the synchronous code, in which case you would crash your server even on expected errors. I am referring to my synchronous code example, it would be insane to call `readFileSync` without a try-catch. – Esailija Oct 23 '13 at 13:41
  • I get it, but I'm not in control of the module code, I need a bullet-proof (or near bullet-proof) was to catch any and all errors as close to the module as possible. HOWEVER - I need to differentiate between a promise.reject and a thrown error. Seems there's no way to do this... – Nick Jennings Oct 24 '13 at 15:08
  • Why do you need to differentiate? In throw/catch-style code, some libraries provide very fine-grained error subclasses, so that you know exactly what happened when you catch. Perhaps think of promise.reject in the same way: if you want to use (e.g) to skip processing, and you "mean" for it to happen, reject() with a unique, recognizable object. Treat everything else as an error. – shaunc May 05 '14 at 23:11
1

Promise rejection is simply a from of failure abstraction. So are node-style callbacks (err, res) and exceptions. Since promises are asynchronous you can't use try-catch to actually catch anything, because errors a likely to happen not in the same tick of event loop.

A quick example:

function test(callback){
    throw 'error';
    callback(null);
}

try {
    test(function () {});
} catch (e) {
    console.log('Caught: ' + e);
}

Here we can catch an error, as function is synchronous (though callback-based). Another:

function test(callback){
    process.nextTick(function () {
        throw 'error';
        callback(null); 
    });
}

try {
    test(function () {});
} catch (e) {
    console.log('Caught: ' + e);
}

Now we can't catch the error! The only option is to pass it in the callback:

function test(callback){
    process.nextTick(function () {
        callback('error', null); 
    });
}

test(function (err, res) {
    if (err) return console.log('Caught: ' + err);
});

Now it's working just like in the first example.The same applies to promises: you can't use try-catch, so you use rejections for error-handling.

vkurchatkin
  • 13,364
  • 2
  • 47
  • 55