201

I'm trying to create a constructor for a blogging platform and it has many async operations going on inside. These range from grabbing the posts from directories, parsing them, sending them through template engines, etc.

So my question is, would it be unwise to have my constructor function return a promise instead of an object of the function they called new against.

For instance:

var engine = new Engine({path: '/path/to/posts'}).then(function (eng) {
   // allow user to interact with the newly created engine object inside 'then'
   engine.showPostsOnOnePage();
});

Now, the user may also not supply a supplement Promise chain link:

var engine = new Engine({path: '/path/to/posts'});

// ERROR
// engine will not be available as an Engine object here

This could pose a problem as the user may be confused why engine is not available after construction.

The reason to use a Promise in the constructor makes sense. I want the entire blog to be functioning after the construction phase. However, it seems like a smell almost to not have access to the object immediately after calling new.

I have debated using something along the lines of engine.start().then() or engine.init() which would return the Promise instead. But those also seem smelly.

Edit: This is in a Node.js project.

6
  • 3
    Is creating the object the asynchronous operation or is acquiring its resources really the asynchronous operation? I think you wouldn't have this problem if you used DI Commented Jun 25, 2014 at 4:29
  • 16
    The most common design pattern I've seen for this type of issue is to just create your object shell in the constructor and then do all the async stuff in an .init() method that can then return the promise. Then you separate out instance data in the object and construction of that object from the async initialization operation. The same issue arises when you have all sorts of different errors (which the caller wants to handle differently) that can occur in the initialization of the object. Much better to return the object from the constructor and then use .init() to return other things. Commented Jun 25, 2014 at 15:29
  • 2
    I'm totally agree with jfriend00. It's better practice to use an init method to make a promise! Commented Jun 26, 2014 at 6:12
  • 1
    @jfriend00 I still don't see why. More code to write and maintain in this approach. Commented Nov 29, 2019 at 14:16
  • 2
    @KarlMorrison - For a discussion of the various techniques to do something asynchronous when creating a new object, see Asynchronous Operations in Constructor. My personal recommendation is a factory function that returns a promise because there is no way to accidentally misuse that pattern and the interface is clear and obvious. Commented Nov 29, 2019 at 16:38

5 Answers 5

246

Yes, it is a bad practice. A constructor should return an instance of its class, nothing else. It would otherwise mess up the new operator and inheritance.

Moreover, a constructor should only create and initialize a new instance. It should set up data structures and all instance-specific properties, but not execute any tasks. It should be a pure function without side effects if possible, with all the benefits that has.

What if I want to execute things from my constructor?

That should go in a method of your class. You want to mutate global state? Then call that procedure explicitly, not as a side effect of generating an object. This call can go right after the instantiation:

var engine = new Engine();
engine.displayPosts();

If that task is asynchronous, you can now easily return a promise for its results from the method, to easily wait until it is finished.
I would however not recommend this pattern when the method (asynchronously) mutates the instance and other methods depend on that, as that would lead to them being required to wait (become async even if they're actually synchronous) and you'd quickly have some internal queue management going on. Do not code instances to exist but be actually unusable.

What if I want to load data into my instance asynchronously?

Ask yourself: Do you actually need the instance without the data? Could you use it somehow?

If the answer to that is No, then you should not create it before you have the data. Make the data ifself a parameter to your constructor, instead of telling the constructor how to fetch the data (or passing a promise for the data).

Then, use a static method to load the data, from which you return a promise. Then chain a call that wraps the data in a new instance on that:

Engine.load({path: '/path/to/posts'}).then(function(posts) {
    new Engine(posts).displayPosts();
});

This allows much greater flexibility in the ways to acquire the data, and simplifies the constructor a lot. Similarly, you might write static factory functions that return promises for Engine instances:

Engine.fromPosts = function(options) {
    return ajax(options.path).then(Engine.parsePosts).then(function(posts) {
        return new Engine(posts, options);
    });
};

…

Engine.fromPosts({path: '/path/to/posts'}).then(function(engine) {
    engine.registerWith(framework).then(function(framePage) {
        engine.showPostsOn(framePage);
    });
});
Sign up to request clarification or add additional context in comments.

8 Comments

How exactly does it "mess up the new operator and inheritance", please? Of course, it returns a promise that resolves to the instance instead of an instance. Could you please explain what this has to do with inheritance?
@mightyiam Well, when (new Engine) instanceof Engine is false, that's definitely unexpected. Similarly, when you try to inherit from that class, super(…) initialises this with a promise not an engine instance - a mess. Don't return from constructors.
@Bergi this is great! but what if the path to load the data is declared in the instance constructor, in your example {path: '/path/to/posts'} this is statically declared, what if you you declare the path within the constructor so the only way to get it is to instantiate it.
@JordanDavis Why can't you move the declaration of the path into the static load function? You might want to ask a new question to share your code and be specific about the goal you have and restrictions you face.
@Bergi thanks for the response, yeah I started to get what you were saying in your question about just creating a static load method, also could use the new await if you declare a static async so like let data = await Engine.load then just pass into constructor.
|
19

I encountered the same problem and came up with this simple solution.

Instead of returning a Promise from the constructor, put it in this._initialized property, like this:

function Engine(path) {
  this._initialized = Promise.resolve()
    .then(() => {
      return doSomethingAsync(path)
    })
    .then((result) => {
      this.resultOfAsyncOp = result
    })
}
  

Then, wrap every method in a callback that runs after the initialization, like that:

Engine.prototype.showPostsOnPage = function () {
  return this._initialized.then(() => {
    // actual body of the method
  })
}

How it looks from the API consumer perspective:

engine = new Engine({path: '/path/to/posts'})
engine.showPostsOnPage()

This works because you can register multiple callbacks to a promise and they run either after it resolves or, if it's already resolved, at the time of attaching the callback.

This is how mongoskin works, except it doesn't actually use promises.


Edit: Since I wrote that reply I've fallen in love with ES6/7 syntax so there's another example using that.

class Engine {
  
  constructor(path) {
    this._initialized = this._initialize(path)
  }

  async _initialize() {
    // actual async constructor logic
    this.resultOfAsyncOp = await doSomethingAsync(path)
  }

  async showPostsOnPage() {
    await this._initialized
    // actual body of the method
  }
  
}

6 Comments

Hm, I don't like this pattern because of the need to "wrap every method". This is just unecessary overhead most times, and complicates many things when methods do return promises while they usually wouldn't need to.
I created npm module which makes wrapping automatic: npmjs.com/package/synchronisify
I know this is an old thread, but to avoid the "wrap every method" issue, at least in Node, a Proxy was useful.
Can also be done with a Proxy instead of having to call await this._initialized in every method
@Purefan Nice! How would you do that?
|
9

To avoid the separation of concerns, use a factory to create the object.

class Engine {
    constructor(data) {
        this.data = data;
    }

    static makeEngine(pathToData) {
        return new Promise((resolve, reject) => {
            getData(pathToData).then(data => {
              resolve(new Engine(data))
            }).catch(reject);
        });
    }
}

2 Comments

Also, avoid answering a different question from the one that was asked.
2

The return value from the constructor replaces the object that the new operator just produced, so returning a promise is not a good idea. Previously, an explicit return value from the constructor was used for the singleton pattern.

The better way in ECMAScript 2017 is to use a static methods: you have one process, which is the numerality of static.

Which method to be run on the new object after the constructor may be known only to the class itself. To encapsulate this inside the class, you can use process.nextTick or Promise.resolve, postponing further execution allowing for listeners to be added and other things in Process.launch, the invoker of the constructor.

Since almost all code executes inside of a Promise, errors will end up in Process.fatal

This basic idea can be modified to fit specific encapsulation needs.

class MyClass {
  constructor(o) {
    if (o == null) o = false
    if (o.run) Promise.resolve()
      .then(() => this.method())
      .then(o.exit).catch(o.reject)
  }

  async method() {}
}

class Process {
  static launch(construct) {
    return new Promise(r => r(
      new construct({run: true, exit: Process.exit, reject: Process.fatal})
    )).catch(Process.fatal)
  }

  static exit() {
    process.exit()
  }

  static fatal(e) {
    console.error(e.message)
    process.exit(1)
  }
}

Process.launch(MyClass)

Comments

0

Yes. It is absolutely bad practice.

Constructor return override is a bad idea in general – outside carefully applied idioms. It can break inheritance, and it especially breaks fields defined in deriving classes. For an example:

const MyCounterMixin = (claſs, startValue = 0) => {
  return class extends claſs {
    #counter = startValue;
    [MyCounterMixin.count]() {
      return this.#counter++;
    }
  };
}
MyCounterMixin.count = Symbol('MyCounterMixin.count');

class GoodCitizen {}
class Snowflake { constructor() { return (async () => this)(); } }

(async () => {
  const GoodCitizenMixin = MyCounterMixin(GoodCitizen);
  const goodCitizen = new GoodCitizenMixin();
  console.log(goodCitizen[MyCounterMixin.count]());   // OK

  const SnowflakeMixin = MyCounterMixin(Snowflake);
  const snowflake = await new SnowflakeMixin();       // or is it just “new”?
  console.log(snowflake[MyCounterMixin.count]());     // TypeError
})().catch(e => {
  console.error(e);
});

The TypeError occurs because the mixin installed its private field on the promise that was returned from the Snowflake constructor, and not on the eventually returned object. Which code should be blamed for this error? I think the Snowflake constructor, because it broke the convention that a constructor ought to return an instance of the class directly. This is crucially relied on by features like super within a constructor. There is no way to write Snowflake such that it both return a promise and be compatible with inheritance.

If you shun inheritance (which, after all, is not a bad idea anyway), you may fail to experience many issues at first – but it’s still defying a strong language convention that a constructor ought to return an instance of its class. TypeScript enforces it as well – the following code is a type-checking error on the return line:

class Snowflake {
  constructor() {
    return (async () => this)();
  }
  foo() {}
}

There is no way to override the type signature for the constructor either: TypeScript will always assume a new expression will return the object directly (if it returns at all). The best you can do is lie to the type checker by writing a type assertion falsely claiming that the IIAFE returns the object instead of a promise – and this is not what type assertions are for: they exist to express true things that the type checker cannot see, like when using FileReader.

Consider also that in modern JavaScript, the preferred syntax for writing a function that returns a promise is to prefix the function with the async keyword. But async constructor() {} has been specifically made a syntax error:

console.log("this will not even parse");

class Invalid {
  async constructor() {
    /* … */
  }
}

This is essentially for the same reason – because a constructor is supposed to return the constructed object, not a promise that fulfils to one. If the TC39 committee intended you to write asynchronous constructors, this syntax would have been allowed.

This is not exclusive to promises – all kinds of careless usage of constructor return overrides would have this problem. That is not to say the feature is entirely useless; you can come up with idioms employing a return override that do not run into such problems (like instance caching or super trickery), but such idioms are rather limited in use and come with constraints (some may even prohibit the use of inheritance). None would allow a constructor to be asynchronous: spawning asynchronous tasks from the constructor would have to be expressed by other means.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.