At the moment, I'm attempting to use async/await
within a class constructor function. This is so that I can get a custom e-mail
tag for an Electron project I'm working on.
customElements.define('e-mail', class extends HTMLElement {
async constructor() {
super()
let uid = this.getAttribute('data-uid')
let message = await grabUID(uid)
const shadowRoot = this.attachShadow({mode: 'open'})
shadowRoot.innerHTML = `
<div id="email">A random email message has appeared. ${message}</div>
`
}
})
At the moment however, the project does not work, with the following error:
Class constructor may not be an async method
Is there a way to circumvent this so that I can use async/await within this? Instead of requiring callbacks or .then()?
You can only use async/await where you can use promises because they are essentially syntax sugar for promises. You can't use promises in a constructor because a constructor must return the object to be constructed, not a promise.
It's possible to call this in the constructor, but you can't await an expression that referenced it.
Just call getWRitings() or whatever async method and don't await it. It won't be done when the constructor ends, but that's ok. Don't use its value there, instead - use its value in another method and call that.
To create an async constructor functions in TypeScript, we can create a factory method. to create the MyClass class that has a private constructor. We use it to create a MyClass instance inside the CreateAsync function. The function is static so we don't need to instantiate MyClass to call it.
This can never work.
The async
keyword allows await
to be used in a function marked as async
but it also converts that function into a promise generator. So a function marked with async
will return a promise. A constructor on the other hand returns the object it is constructing. Thus we have a situation where you want to both return an object and a promise: an impossible situation.
You can only use async/await where you can use promises because they are essentially syntax sugar for promises. You can't use promises in a constructor because a constructor must return the object to be constructed, not a promise.
There are two design patterns to overcome this, both invented before promises were around.
Use of an init()
function. This works a bit like jQuery's .ready()
. The object you create can only be used inside it's own init
or ready
function:
Usage:
var myObj = new myClass();
myObj.init(function() {
// inside here you can use myObj
});
Implementation:
class myClass {
constructor () {
}
init (callback) {
// do something async and call the callback:
callback.bind(this)();
}
}
Use a builder. I've not seen this used much in javascript but this is one of the more common work-arounds in Java when an object needs to be constructed asynchronously. Of course, the builder pattern is used when constructing an object that requires a lot of complicated parameters. Which is exactly the use-case for asynchronous builders. The difference is that an async builder does not return an object but a promise of that object:
Usage:
myClass.build().then(function(myObj) {
// myObj is returned by the promise,
// not by the constructor
// or builder
});
// with async/await:
async function foo () {
var myObj = await myClass.build();
}
Implementation:
class myClass {
constructor (async_param) {
if (typeof async_param === 'undefined') {
throw new Error('Cannot be called directly');
}
}
static build () {
return doSomeAsyncStuff()
.then(function(async_result){
return new myClass(async_result);
});
}
}
Implementation with async/await:
class myClass {
constructor (async_param) {
if (typeof async_param === 'undefined') {
throw new Error('Cannot be called directly');
}
}
static async build () {
var async_result = await doSomeAsyncStuff();
return new myClass(async_result);
}
}
Note: although in the examples above we use promises for the async builder they are not strictly speaking necessary. You can just as easily write a builder that accept a callback.
This has nothing whatsoever to do with async constructors but with what the keyword this
actually mean (which may be a bit surprising to people coming from languages that do auto-resolution of method names, that is, languages that don't need the this
keyword).
The this
keyword refers to the instantiated object. Not the class. Therefore you cannot normally use this
inside static functions since the static function is not bound to any object but is bound directly to the class.
That is to say, in the following code:
class A {
static foo () {}
}
You cannot do:
var a = new A();
a.foo() // NOPE!!
instead you need to call it as:
A.foo();
Therefore, the following code would result in an error:
class A {
static foo () {
this.bar(); // you are calling this as static
// so bar is undefinned
}
bar () {}
}
To fix it you can make bar
either a regular function or a static method:
function bar1 () {}
class A {
static foo () {
bar1(); // this is OK
A.bar2(); // this is OK
}
static bar2 () {}
}
You can definitely do this, by returning an Immediately Invoked Async Function Expression from the constructor. IIAFE
is the fancy name for a very common pattern that was required in order to use await
outside of an async function, before top-level await became available:
(async () => {
await someFunction();
})();
We'll be using this pattern to immediately execute the async function in the constructor, and return its result as this
:
// Sample async function to be used in the async constructor
async function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
class AsyncConstructor {
constructor(value) {
return (async () => {
// Call async functions here
await sleep(500);
this.value = value;
// Constructors return `this` implicitly, but this is an IIFE, so
// return `this` explicitly (else we'd return an empty object).
return this;
})();
}
}
(async () => {
console.log('Constructing...');
const obj = await new AsyncConstructor(123);
console.log('Done:', obj);
})();
To instantiate the class, use:
const instance = await new AsyncConstructor(...);
For TypeScript, you need to assert that the type of the constructor is the class type, rather than a promise returning the class type:
class AsyncConstructor {
constructor(value) {
return (async (): Promise<AsyncConstructor> => {
// ...
return this;
})() as unknown as AsyncConstructor; // <-- type assertion
}
}
super
in the constructor of the derived class, you'll have to call it without await
. If you need to call the super constructor with await
, you'll run into TypeScript error 2337: Super calls are not permitted outside constructors or in nested functions inside constructors.
Before using this solution, determine whether you'll need to extend the class, and document that the constructor must be called with await
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With