Problem
I’m now attempting to use async/await within a constructor method for a class. This is so that I may create a custom e-mail tag for a project I’m working on with Electron.
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>
`
}
})
However, the project is now unusable due to the following error:
Class constructor may not be an async method
Is there any way to get around this so that I can utilize async/await? Instead of callbacks or.then(), why not?
Asked by Alexander Craggs
Solution #1
This is never going to work.
The async keyword allows await to be used in an async function while also converting it into a promise generator. As a result, an async function will return a promise. On the other hand, a function Object() { [native code] } returns the object it is creating. As a result, you’re in a situation where you need to return both an object and a promise: it’s an impossible circumstance.
Because async/await is effectively syntax sugar for promises, you can only use it where promises are allowed. Promises aren’t allowed in constructors since they must return the object being constructed, not a promise.
There are two design patterns that can be used to get around this, both of which were created before promises existed.
This has nothing to do with async constructors and everything to do with what the keyword this means (which may come as a surprise to individuals coming from languages that do auto-resolution of method names, i.e. languages that don’t require the this keyword).
The instantiated object is referred to via the this keyword. It isn’t the class. As a result, you can’t utilize this inside static functions because the static function isn’t connected to any object, but rather to the class.
That is to say, in the code below:
class A {
static foo () {}
}
You cannot do:
var a = new A();
a.foo() // NOPE!!
Instead, you should refer to it as:
A.foo();
As a result, the following code will generate an error:
class A {
static foo () {
this.bar(); // you are calling this as static
// so bar is undefinned
}
bar () {}
}
You may fix it by making bar a regular function or a static method.
function bar1 () {}
class A {
static foo () {
bar1(); // this is OK
A.bar2(); // this is OK
}
static bar2 () {}
}
Answered by slebetman
Solution #2
By returning an Immediately Invoked Async Function Expression from the constructor, you can accomplish this. Before top-level await became accessible, IIAFE was the fancy name for a fairly common pattern that was required in order to use await outside of an async function:
(async () => {
await someFunction();
})();
This pattern will be used to execute the async function in the constructor immediately and return the result as this.
To create a new instance of the class, type:
const instance = await new AsyncConstructor(...);
Instead of a promise returning the class type, you must assert that the constructor’s type is the class type in TypeScript:
class AsyncConstructor {
constructor(value) {
return (async (): Promise<AsyncConstructor> => {
// ...
return this;
})() as unknown as AsyncConstructor; // <-- type assertion
}
}
Determine whether you’ll need to extend the class before utilizing this approach, and document that the constructor must be called with await.
Answered by Downgoat
Solution #3
Because async functions are promises, you can make a static function on your class that runs an async function and returns the class instance:
class Yql {
constructor () {
// Set up your class
}
static init () {
return (async function () {
let yql = new Yql()
// Do async stuff
await yql.build()
// Return instance
return yql
}())
}
async build () {
// Do stuff with await if needed
}
}
async function yql () {
// Do this instead of "new Yql()"
let yql = await Yql.init()
// Do stuff with yql instance
}
yql()
Let yql = await is called with let yql = await. From an async method, use Yql.init().
Answered by Vidar
Solution #4
You can make it work, despite what others have said.
The constructor of a JavaScript class can return anything, including an instance of another class. As a result, the constructor of your class can return a Promise that resolves to its actual instance.
Here’s an illustration:
export class Foo {
constructor() {
return (async () => {
// await anything you want
return this; // Return the newly-created instance
})();
}
}
Then you’ll make Foo instances like this:
const foo = await new Foo();
Answered by Davide Cannizzo
Solution #5
Instead of doing new MyClass, you can write an async init()… return this; function (). whenever you’d typically say new MyClass, use init() ().
This isn’t tidy since it assumes that everyone who uses your code, including yourself, will always initialize the object in this manner. If you’re only utilizing this object in a couple of places in your code, however, it might be fine.
However, because ES lacks a type system, if you neglect to call it, you’ll merely get undefined because the constructor returns null. Oops. Much better to do something like this:
The best course of action is to:
class AsyncOnlyObject {
constructor() {
}
async init() {
this.someField = await this.calculateStuff();
}
async calculateStuff() {
return 5;
}
}
async function newAsync_AsyncOnlyObject() {
return await new AsyncOnlyObject().init();
}
newAsync_AsyncOnlyObject().then(console.log);
// output: AsyncOnlyObject {someField: 5}
You should probably simply write a factory method that utilizes Object.create(AsyncOnlyObject.prototype) directly instead of doing new AsyncOnlyObject:
async function newAsync_AsyncOnlyObject() {
return await Object.create(AsyncOnlyObject.prototype).init();
}
newAsync_AsyncOnlyObject().then(console.log);
// output: AsyncOnlyObject {someField: 5}
However, suppose you want to use this pattern on a large number of objects… you could abstract this as a decorator or something you (verbosely, ugh) call after defining like postProcess makeAsyncInit(AsyncOnlyObject), but I’m going to use extends because it fits into subclass semantics (subclasses are parent classes + extra, in that they should obey the parent class’s design contract and may do additional things;
class AsyncObject {
constructor() {
throw new Error('classes descended from AsyncObject must be initialized as (await) TheClassName.anew(), rather than new TheClassName()');
}
static async anew(...args) {
var R = Object.create(this.prototype);
R.init(...args);
return R;
}
}
class MyObject extends AsyncObject {
async init(x, y=5) {
this.x = x;
this.y = y;
// bonus: we need not return 'this'
}
}
MyObject.anew('x').then(console.log);
// output: MyObject {x: "x", y: 5}
(Not for production use: I haven’t considered complex circumstances like whether this is the right method to construct a wrapper for keyword parameters.)
Answered by ninjagecko
Post is based on https://stackoverflow.com/questions/43431550/async-await-class-constructor