Skip to content

Instantly share code, notes, and snippets.

@goloroden
Last active June 22, 2023 02:10
Show Gist options
  • Star 19 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save goloroden/c976971e5f42c859f64be3ad7fc6f4ed to your computer and use it in GitHub Desktop.
Save goloroden/c976971e5f42c859f64be3ad7fc6f4ed to your computer and use it in GitHub Desktop.
Async constructors for JavaScript
// In JavaScript, constructors can only be synchronous right now. This makes sense
// from the point of view that a constructor is a function that returns a newly
// initialized object.
//
// On the other hand, it would sometimes be very handy, if one could have async
// constructors, e.g. when a class represents a database connection, and it is
// desired to establish the connection when you create a new instance.
//
// I know that there have been discussions on this on StackOverflow & co., but
// the so-far mentioned counter arguments (such as: doesn't work as expected
// with new) didn't convince me. Actually, it works as expected (see below),
// as long as you use await – but this is true for any async function ;-)
//
// Could this be an idea for TC39?
'use strict';
const { promisify } = require('util');
const sleep = promisify(setTimeout);
class Example {
// This is what I would like to have.
// async constructor () {
// await sleep(1000);
// this.value = 23;
// }
// This is how it could be implemented.
constructor () {
return new Promise(async (resolve, reject) => {
try {
await sleep(1000);
this.value = 23;
} catch (ex) {
return reject(ex);
}
resolve(this);
});
}
}
(async () => {
// It works as expected, as long as you use the await keyword.
const example = await new Example();
console.log(example instanceof Example);
console.log(example.value);
})();
@rauschma
Copy link

rauschma commented Feb 13, 2018

I’d introduce a factory function:

  • Intuitively, I expect new Example() to work synchronously.
  • The type new Example() should be Example, but in this example, it is actually Promise<Example>.

@silasg
Copy link

silasg commented Feb 13, 2018

As already discussed on Twitter:

FP perspective: Constructor is just a function returning a data structure (which is an object in that case). Such a function can be async, sure.

However, from an OO perspective an async constructor would be a violation of the Principle of least astonishment/surprise (POLA/PLS) since I expect constructor invocation not to cause long running (async) side effects like IO or expensive calculations.

Since constructors are more OO style, I think I would probably argue like the latter. However, this is just a first impression and I did not think (or searched the web) about this discussion.

@goloroden
Copy link
Author

@rauschma @silasg Thanks for your answers!

Actually, intuitively I'd also expect any Promise-based function to work synchronously, as the function's name does not give any hint that it returns a promise. We don't use readFileAsPromise or readFileAsync or something as that, it's just readFile. That has already been the same with callbacks. I can not tell from looking at a function's name that map is synchronous, while readFile is not. I can only guess that the latter one is async, as it (probably) does I/O. But that's just guessing. To be sure I have to look up the signature.

The same is true if we had async constructors: Yes, it's not possible to (intuitively) tell from looking at the name whether it's synchronous or asynchronous, but again I could guess it, and to be sure I could look it up.

In the same way that a result of readFile is not a file, but a promise (which is apparently fine for everyone), I think that it could be fine as well if the result of an asynchronous constructor would be a promise.

My point is: There are situations where it makes sense, and it's technically possible and feasible, as the example code shows. Having an additional init function or using a factory function works, but is cumbersome and tends to be error-prone, because people forget that it's not enough to create a connection, but they also have to initialize it (which, in everyday's language, is the task of the constructor, isn't it?).

To cut a long story short:

  • It would make life easier.
  • It is technically possible.
  • It is conformant to all the other JS APIs.

So, why should we not allow constructors to be async?
IMHO this doesn't make sense.

Now it's your turn, again 😉
What do you think?

@Fenzland
Copy link

I'm trying to write code like this:

class Foo
{
    async constructor(){}
}

but not work. Then, I search and found your gist. Thank you for your idea.
Async constructor is necessary for async coding. In some case, a object cannot be construct syncly. Instead of getting a useless object and don't know when it'll work, receiving a promise is more helpful.

Here is my async constructor:

class Foo
{
    constructor(){
        return (async ()=> {
            this.foo= await somePromise;
            return this;
        })();
    }
}

@zanethomas
Copy link

zanethomas commented Apr 27, 2019

Here's something I've been playing with the past couple days

function Post(db, post) {
  if (!new.target) {
    return new Post(db, post);
  }
  this.db = db;
  this.post = post;
  //
  // create new post
  //
  return new Promise(async (resolve, reject) => {
    try {
      //
      // if we have an id then the post is already in the
      // database and we have already loaded it!
      //
      if (!post.id) {
        let id = await this._insert(post);
        this.post.id = id[0];
      }
      resolve(this);
    } catch (err) {
      reject(err);
    }
  });
}
  let post = await Posts.create({
    title: 'title',
    body: 'body',
    author_id: author.id
  });
Posts.create = async function(params) {
  return Post(state.db, params);
}

@Dimtime
Copy link

Dimtime commented Aug 26, 2019

I have same idea - we need it. I try to put that idea in standard, but i need more support...
My discuss of proposal here:
CVzP-DINO3s

@Dimtime
Copy link

Dimtime commented Aug 27, 2019

How it says: "there is no limit to perfection" - new wrapper
Now it works like more native:

class PromiseClass {
static async new(test='test'){ this.promise= test; return this;}
constructor(...args) {
let s = async()=>PromiseClass.new.call(this,...args);
return (async r=>await s() )(); 
}//new 
}//class 
class AsyncClass extends PromiseClass{
static async new(){ return this; }
constructor(...args){ 
let s = async()=>{ await super(); return AsyncClass.new.call(this,...args); };
return (async r=>await s() )(); 
}//new 
}//AsyncClass

@Dimtime
Copy link

Dimtime commented Aug 27, 2019

We continue discuss here.
All we need just right words i think. Seems async class or async constructor - too bad.
Maybe promise_class sounds more good...

@Dimtime
Copy link

Dimtime commented Feb 12, 2020

@Dimtime
Copy link

Dimtime commented Aug 2, 2020

My new version of wrapper and you can send your variants - tc39/proposal-async-init#7

class PromiseClass {
static async $new(){this.promise='promise'; return this;}
constructor(...args) {return this.constructor.$new.call(this,...args);}//new
}//class

class AsyncClass extends PromiseClass{
static async $new(){super.$new.call(this); this.async='async'; return this;}
constructor(...args){return super(...args)}//new
}//AsyncClass

@mannharleen
Copy link

Here's something I've been playing with the past couple days

function Post(db, post) {
  if (!new.target) {
    return new Post(db, post);
  }
  this.db = db;
  this.post = post;
  //
  // create new post
  //
  return new Promise(async (resolve, reject) => {
    try {
      //
      // if we have an id then the post is already in the
      // database and we have already loaded it!
      //
      if (!post.id) {
        let id = await this._insert(post);
        this.post.id = id[0];
      }
      resolve(this);
    } catch (err) {
      reject(err);
    }
  });
}
  let post = await Posts.create({
    title: 'title',
    body: 'body',
    author_id: author.id
  });
Posts.create = async function(params) {
  return Post(state.db, params);
}

good one!

@bakasura980
Copy link

bakasura980 commented Jan 6, 2022

There is one more way of doing an async constructor - Proxy

Typescript:

class A {

    private data: any
    private info: any

    public constructor (data: any, info: string) {
        this.data = data
        this.info = info
    }

}

interface InterfaceA {
    new(info: string): A;
}

class AProxy {

    public static init (): InterfaceA {
        const proxyHandler = {
            construct: async function (args: any) {
                // Some async operation
                const data = await retrieve()
                return new A(data, args)
            }
        }

        return new Proxy(A, proxyHandler)
    }

}

export default AProxy.init()

import A from './a.ts'

(async () => {
    const a = await new A('Some iNFO')
})()

Javascript:

class A {
    constructor (data, info) {
        this.data = data
        this.info = info
    }
}

class AProxy {

    static init () {
        const proxyHandler = {
            construct: async function (args) {
                // Some async operation
                const data = await retrieve()
                return new A(data, args)
            }
        }

        return new Proxy(A, proxyHandler)
    }

}

module.exports = AProxy.init()

But to be honest, if you need an async constructor, it is always better to use the build pattern as it is more readable and understandable

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment