-
-
Save goloroden/c976971e5f42c859f64be3ad7fc6f4ed to your computer and use it in GitHub Desktop.
// 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); | |
})(); |
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.
@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?
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;
})();
}
}
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);
}
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:
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
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...
Update: now we have proposal 1 stage
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
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!
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
I’d introduce a factory function:
new Example()
to work synchronously.new Example()
should beExample
, but in this example, it is actuallyPromise<Example>
.