53

How can I best handle a situation like the following?

I have a constructor that takes a while to complete.

var Element = function Element(name){
   this.name = name;
   this.nucleus = {};

   this.load_nucleus(name); // This might take a second.
}

var oxygen = new Element('oxygen');
console.log(oxygen.nucleus); // Returns {}, because load_nucleus hasn't finished.

I see three options, each of which seem out of the ordinary.

One, add a callback to the constructor.

var Element = function Element(name, fn){
   this.name = name;
   this.nucleus = {};

   this.load_nucleus(name, function(){
      fn(); // Now continue.
   });
}

Element.prototype.load_nucleus(name, fn){
   fs.readFile(name+'.json', function(err, data) {
      this.nucleus = JSON.parse(data); 
      fn();
   });
}

var oxygen = new Element('oxygen', function(){  
   console.log(oxygen.nucleus);
});

Two, use EventEmitter to emit a 'loaded' event.

var Element = function Element(name){
   this.name = name;
   this.nucleus = {};

   this.load_nucleus(name); // This might take a second.
}

Element.prototype.load_nucleus(name){
   var self = this;
   fs.readFile(name+'.json', function(err, data) {
      self.nucleus = JSON.parse(data); 
      self.emit('loaded');
   });
}

util.inherits(Element, events.EventEmitter);

var oxygen = new Element('oxygen');
oxygen.once('loaded', function(){
   console.log(this.nucleus);
});

Or three, block the constructor.

var Element = function Element(name){
   this.name = name;
   this.nucleus = {};

   this.load_nucleus(name); // This might take a second.
}

Element.prototype.load_nucleus(name, fn){
   this.nucleus = JSON.parse(fs.readFileSync(name+'.json'));
}

var oxygen = new Element('oxygen');
console.log(oxygen.nucleus)

But I haven't seen any of this done before.

What other options do I have?

Luke Burns
  • 1,911
  • 3
  • 24
  • 30
  • 4
    Blocking the constructor means blocking ***everything***, so I probably wouldn't do that. – josh3736 Aug 08 '12 at 02:22
  • Options 1, and 2 are effectively the same approach. You pass a load handler to the constructor, and that handler is triggered from within the readFile callback. This is how event-based programming works (which is what JavaScript/Node is). You can use option 1, or 2, or any other variation on the theme, but effectively you get the same asynchronous behavior. – Šime Vidas Aug 08 '12 at 02:42
  • 1
    I learnt much more things from this "question" than given answers. +1 for "informative question"! – scaryguy Feb 09 '15 at 00:13
  • 2
    Just one things. Use `.once()` not `.on()` for this case. It's sementically better :). – Cyril ALFARO Feb 16 '15 at 23:09
  • I'm looking for a way to do it blocking with ES6 Classes, is that possible? Thank you – DiegoRBaquero Jan 02 '17 at 07:01
  • @DiegoRBaquero You should’t block. Use promises instead. – Demi Apr 12 '17 at 18:31

7 Answers7

29

Update 2: Here is an updated example using an asynchronous factory method. N.B. this requires Node 8 or Babel if run in a browser.

class Element {
    constructor(nucleus){
        this.nucleus = nucleus;
    }

    static async createElement(){
        const nucleus = await this.loadNucleus();
        return new Element(nucleus);
    }

    static async loadNucleus(){
        // do something async here and return it
        return 10;
    }
}

async function main(){
    const element = await Element.createElement();
    // use your element
}

main();

Update: The code below got upvoted a couple of times. However I find this approach using a static method much better: https://stackoverflow.com/a/24686979/2124586

ES6 version using promises

class Element{
    constructor(){
        this.some_property = 5;
        this.nucleus;

        return new Promise((resolve) => {
            this.load_nucleus().then((nucleus) => {
                this.nucleus = nucleus;
                resolve(this);
            });
        });
    }

    load_nucleus(){
        return new Promise((resolve) => {
            setTimeout(() => resolve(10), 1000)
        });
    }
}

//Usage
new Element().then(function(instance){
    // do stuff with your instance
});
tim
  • 3,191
  • 2
  • 15
  • 17
  • 2
    The most elegant one. – Zlatko Oct 29 '15 at 15:43
  • 2
    Related: http://stackoverflow.com/questions/24398699/is-it-bad-practice-to-have-a-constructor-function-return-a-promise – Robby Cornelissen Feb 03 '16 at 06:58
  • 4
    the weird thing about this is that your constructor doesn't return the instance, it returns a promise to the instance, which is a bit confusing for an OOP pattern. – JBCP Sep 29 '16 at 19:13
  • 4
    I agree. These days I would probably go for an async static function that loads the data and then returns instances with the data populated. – tim Sep 30 '16 at 09:02
  • An async factory method is the best solution for such cases. A constructor should always return an instance of the class. Even a promise for an instance of the class is not good enough. – omer Jan 06 '20 at 10:30
11

Given the necessity to avoid blocking in Node, the use of events or callbacks isn't so strange(1).

With a slight edit of Two, you could merge it with One:

var Element = function Element(name, fn){
    this.name = name;
    this.nucleus = {};

    if (fn) this.on('loaded', fn);

    this.load_nucleus(name); // This might take a second.
}

...

Though, like the fs.readFile in your example, the core Node APIs (at least) often follow the pattern of static functions that expose the instance when the data is ready:

var Element = function Element(name, nucleus) {
    this.name = name;
    this.nucleus = nucleus;
};

Element.create = function (name, fn) {
    fs.readFile(name+'.json', function(err, data) {
        var nucleus = err ? null : JSON.parse(data);
        fn(err, new Element(name, nucleus));
    });
};

Element.create('oxygen', function (err, elem) {
    if (!err) {
        console.log(elem.name, elem.nucleus);
    }
});

(1) It shouldn't take very long to read a JSON file. If it is, perhaps a change in storage system is in order for the data.

Jonathan Lonowski
  • 121,453
  • 34
  • 200
  • 199
4

I have developed an async constructor:

function Myclass(){
 return (async () => {
     ... code here ...
     return this;
 })();
}

(async function() { 
 let s=await new Myclass();
 console.log("s",s)
})();
  • async returns a promise
  • arrow functions pass 'this' as is
  • it is possible to return something else when doing new (you still get a new empty object in this variable. if you call the function without new. you get the original this. like maybe window or global or its holding object).
  • it is possible to return the return value of called async function using await.
  • to use await in normal code, need to wrap the calls with an async anonymous function, that is called instantly. (the called function returns promise and code continues)

my 1st iteration was:

maybe just add a callback

call an anonymous async function, then call the callback.

function Myclass(cb){
 var asynccode=(async () => { 
     await this.something1();
     console.log(this.result)
 })();

 if(cb)
    asynccode.then(cb.bind(this))
}

my 2nd iteration was:

let's try with a promise instead of a callback. I thought to myself: strange a promise returning a promise, and it worked. .. so the next version is just a promise.

function Myclass(){
 this.result=false;
 var asynccode=(async () => {
     await new Promise (resolve => setTimeout (()=>{this.result="ok";resolve()}, 1000))
     console.log(this.result)
     return this;
 })();
 return asynccode;
}


(async function() { 
 let s=await new Myclass();
 console.log("s",s)
})();

callback-based for old javascript

function Myclass(cb){
 var that=this;
 var cb_wrap=function(data){that.data=data;cb(that)}
 getdata(cb_wrap)
}

new Myclass(function(s){

});
Shimon Doodkin
  • 4,310
  • 34
  • 37
  • 5
    Why has this been downvoted with no explanation? It is a significant attempt to provide a workable solution to the stated problem. If you downvote because you think this approach will lead to problems then explain your concern in comments. – Peter Wone Nov 12 '17 at 08:49
3

One thing you could do is preload all the nuclei (maybe inefficient; I don't know how much data it is). The other, which I would recommend if preloading is not an option, would involve a callback with a cache to save loaded nuclei. Here is that approach:

Element.nuclei = {};

Element.prototype.load_nucleus = function(name, fn){
   if ( name in Element.nuclei ) {
       this.nucleus = Element.nuclei[name];
       return fn();
   }
   fs.readFile(name+'.json', function(err, data) {
      this.nucleus = Element.nuclei[name] = JSON.parse(data); 
      fn();
   });
}
Will
  • 19,661
  • 7
  • 47
  • 48
2

This is a bad code design.

The main problem is in the callback your instance it's not still execute the "return", this is what I mean

var MyClass = function(cb) {
  doAsync(function(err) {
    cb(err)
  }

  return {
    method1: function() { },
    method2: function() { }
  }
}

var _my = new MyClass(function(err) {
  console.log('instance', _my) // < _my is still undefined
  // _my.method1() can't run any methods from _my instance
})
_my.method1() // < it run the function, but it's not yet inited

So, the good code design is to explicitly call the "init" method (or in your case "load_nucleus") after instanced the class

var MyClass = function() {
  return {
    init: function(cb) {
      doAsync(function(err) {
        cb(err)
      }
    },
    method1: function() { },
    method2: function() { }
  }
}

var _my = new MyClass()
_my.init(function(err) { 
   if(err) {
     console.error('init error', err)
     return
   } 
   console.log('inited')
  // _my.method1()
})
Simone Sanfratello
  • 1,520
  • 1
  • 10
  • 21
1

I extract out the async portions into a fluent method. By convention I call them together.

class FooBar {
  constructor() {
    this.foo = "foo";
  }
  
  async create() {
    this.bar = await bar();

    return this;
  }
}

async function bar() {
  return "bar";
}

async function main() {
  const foobar = await new FooBar().create(); // two-part constructor
  console.log(foobar.foo, foobar.bar);
}

main(); // foo bar

I tried a static factory approach wrapping new FooBar(), e.g. FooBar.create(), but it didn't play well with inheritance. If you extend FooBar into FooBarChild, FooBarChild.create() will still return a FooBar. Whereas with my approach new FooBarChild().create() will return a FooBarChild and it's easy to setup an inheritance chain with create().

Bill Barnes
  • 344
  • 1
  • 8
-1

You can run constructor function with async functions synchronously via nsynjs. Here is an example to illustrate:

index.js (main app logic):

var nsynjs = require('nsynjs');
var modules = {
    MyObject: require('./MyObject')
};

function synchronousApp(modules) {
    try {
        var myObjectInstance1 = new modules.MyObject('data1.json');
        var myObjectInstance2 = new modules.MyObject('data2.json');

        console.log(myObjectInstance1.getData());
        console.log(myObjectInstance2.getData());
    }
    catch (e) {
        console.log("Error",e);
    }
}

nsynjs.run(synchronousApp,null,modules,function () {
        console.log('done');
});

MyObject.js (class definition with slow constructor):

var nsynjs = require('nsynjs');

var synchronousCode = function (wrappers) {
    var config;

    // constructor of MyObject
    var MyObject = function(fileName) {
        this.data = JSON.parse(wrappers.readFile(nsynjsCtx, fileName).data);
    };
    MyObject.prototype.getData = function () {
        return this.data;
    };
    return MyObject;
};

var wrappers = require('./wrappers');
nsynjs.run(synchronousCode,{},wrappers,function (m) {
    module.exports = m;
});

wrappers.js (nsynjs-aware wrapper around slow functions with callbacks):

var fs=require('fs');
exports.readFile = function (ctx,name) {
    var res={};
    fs.readFile( name, "utf8", function( error , configText ){
        if( error ) res.error = error;
        res.data = configText;
        ctx.resume(error);
    } );
    return res;
};
exports.readFile.nsynjsHasCallback = true;

Full set of files for this example could be found here: https://github.com/amaksr/nsynjs/tree/master/examples/node-async-constructor

amaksr
  • 7,555
  • 2
  • 16
  • 17