Search code examples
javascriptnode.jsasynchronousstack-tracecontrol-flow

"Call stack" for callbacks in node.js


I'm used to thinking in Java and I'm trying to get my head around node.js. My program needs to log information when things go wrong, and I find I'm having to put in a lot of boilerplate code in my node.js program to get what I'd get for free in Java.

My question boils down to:

  • is there an easier/non-boilerplate way to get stack-like information in a chain of callbacks? and/or
  • am I guilty of failing to grasp node.js properly, and trying to force asynchronous node.js to be more like synchronous Java?

Java Example

Here's a noddy Java program which tries (and fails) to connect to a Mongo database: import java.net.UnknownHostException;

import com.mongodb.Mongo;

public class Test {

    public static void main(final String[] args) throws UnknownHostException {
        final Mongo mongo = a();
    }

    private static Mongo a() throws UnknownHostException {
        return b();
    }

    private static Mongo b() throws UnknownHostException {
        return c();
    }

    private static Mongo c() throws UnknownHostException {
        return new Mongo("non-existent host");
    }

}

...which gives this helpful stack output:

Exception in thread "main" java.net.UnknownHostException: non-existent host
at java.net.Inet6AddressImpl.lookupAllHostAddr(Native Method)
at java.net.InetAddress$1.lookupAllHostAddr(Unknown Source)
at java.net.InetAddress.getAddressesFromNameService(Unknown Source)
at java.net.InetAddress.getAllByName0(Unknown Source)
at java.net.InetAddress.getAllByName(Unknown Source)
at java.net.InetAddress.getAllByName(Unknown Source)
at java.net.InetAddress.getByName(Unknown Source)
at com.mongodb.ServerAddress.updateInetAddress(ServerAddress.java:204)
at com.mongodb.ServerAddress.<init>(ServerAddress.java:73)
at com.mongodb.ServerAddress.<init>(ServerAddress.java:46)
at com.mongodb.Mongo.<init>(Mongo.java:138)
at Test.c(Test.java:20)
at Test.b(Test.java:16)
at Test.a(Test.java:12)
at Test.main(Test.java:8)

(In particular, the last 4 lines show me "what was happening" in my own code at the time the Mongo error occurred.)

Node.js Example

Here's my attempt to re-write my program in node.js:

a(function (err, mongo) {
    if (err) {
        console.log("Something went wrong in main");
        console.log(err);
    }
});

function a(callback) {
    b(function (err, mongo) {
        if (err) {
            console.log("Something went wrong in a()");
            return callback(err);
        }

        return callback(null, mongo);
    });
}

function b(callback) {
    c(function (err, mongo) {
        if (err) {
            console.log("Something went wrong in b()");
            return callback(err);
        }

        return callback(null, mongo);
    });
}

function c(callback) {
    var MongoClient = require('mongodb').MongoClient;
    return MongoClient.connect('mongodb://non-existent host/', function (err, mongo) {
        if (err) {
            console.log("Something went wrong in c()");
            return callback(err);
        }

        return callback(null, mongo);
    });
}

...which gives this output:

Something went wrong in c()
Something went wrong in b()
Something went wrong in a()
Something went wrong in main
[Error: failed to connect to [non-existent host:27017]]

But to get this output, I have to put in lots of boilerplate code throughout my program, which is going to be a pain to police as my program gets larger and I have a whole development team.

Can I get this stack-like output another way? Is it un-node-like to expect this kind of output?


Solution

  • Promises are exactly what you are looking for (bring back the stack features to async code)

    var Promise = require("bluebird");
    var mongodb = require("mongodb");
    // enable long stack traces, bluebird specific
    Promise.longStackTraces();
    // promisify mongodb so that it returns promises, also bluebird specific
    Promise.promisifyAll(mongodb);
    // raise stack limit, feature of v8/node.js
    Error.stackTraceLimit = 100;
    
    
    function c() {
        var MongoClient = require("mongodb").MongoClient;
        return MongoClient.connectAsync('mongodb://non-existent host/')
    }
    
    function b() {
        return c()
    }
    
    function a() {
        return b()
    }
    
    a().then(function(connection) {
    
    });
    

    Gives:

    Possibly unhandled Error: failed to connect to [non-existent host:27017]
        at null.<anonymous> (/home/petka/bluebird/node_modules/mongodb/lib/mongodb/connection/server.js:546:74)
        at EventEmitter.emit (events.js:106:17)
        at null.<anonymous> (/home/petka/bluebird/node_modules/mongodb/lib/mongodb/connection/connection_pool.js:150:15)
        at EventEmitter.emit (events.js:98:17)
        at Socket.<anonymous> (/home/petka/bluebird/node_modules/mongodb/lib/mongodb/connection/connection.js:533:10)
        at Socket.EventEmitter.emit (events.js:95:17)
        at net.js:830:16
    From previous event:
        at Function.connectAsync (eval at makeNodePromisifiedEval (/home/petka/bluebird/js/main/promisify.js:199:12), <anonymous>:7:21)
        at c (/home/petka/bluebird/throwaway.js:10:28)
        at b (/home/petka/bluebird/throwaway.js:14:16)
        at a (/home/petka/bluebird/throwaway.js:18:16)
        at Object.<anonymous> (/home/petka/bluebird/throwaway.js:21:5)
        at Module._compile (module.js:456:26)
        at Object.Module._extensions..js (module.js:474:10)
        at Module.load (module.js:356:32)
        at Function.Module._load (module.js:312:12)
        at Function.Module.runMain (module.js:497:10)
        at startup (node.js:119:16)
        at node.js:902:3
    

    You can use catch (named so because it works like a real catch statement) in one place:

     a().catch(function(e) {
          //handle e
     });
    

    Also bluebird specific features added to catch:

    Predicated catches are also supported since it's just a method:

     a().catch(SyntaxError, function(e) {
    
     });
    

    Predicate can be an error constructor or a predicate function

     // define a predicate for IO errors
     function IOError(e) {
         return "code" in Object(e);  
     }