DEV Community

Discussion on: The Proper Way to Write Async Constructors in JavaScript

Collapse
 
dandv profile image
Dan Dascalescu • Edited

How about the well-documented (since 2018) method of returning an Immediately-Invoked Async Function Expression from the constructor? Much simpler than the two workarounds, and more idiomatic than the static async factory functions. It's simply,

const obj = await new AsyncConstructor(...);
Enter fullscreen mode Exit fullscreen mode
Collapse
 
somedood profile image
Basti Ortiz • Edited

Well, we must first recall that async functions always return Promise instances. Hence, returning an async IIFE from a constructor is the same as if we returned a plain Promise, which is exactly what I argued against for Workaround #1. The main issue is that although this is acceptable according to the ECMAScript Standard (which allows arbitrary values to be returned from a constructor), it is sub-optimal in that it is surprising and even unexpected for most users—even to the TypeScript type inference engine! You can refer to the section about Workaround #1 for more details on why this is not ideal.

Collapse
 
dandv profile image
Dan Dascalescu

I've read the 5 drawbacks of Workaround #1, but I don't quite see how they apply to the code I'm proposing. async-await works, there's no chaining of Promises, yes we use an arrow function to preserve this (is that a big deal?), and the last two points are unclear.

Thread Thread
 
somedood profile image
Basti Ortiz • Edited

Ah, my apologies. I was not clear enough with my phrasing. What I meant is that the async IIFE you're proposing is semantically equivalent to the chained promises I presented in the article—just with some nice syntax. If we were to transform the async IIFE into their chained equivalent, then the points I made in Workaround #1 are still applicable—again, just with nicer syntax.

My main issue lies in the atypical semantics of returning Promise<this> (assuming such syntax exists) from the constructor. Normally, I expect that a class constructor returns an instance of that class. However, using async IIFEs (or chained promises otherwise) goes against this expectation since it does not return this but Promise<this> instead.

The deal breaker, then, is the fact that although this technique is allowed and correct in JavaScript, it is difficult to deny that it is unexpected unless documented. In my opinion, it is best to explicitly tell the user (through the type signature) that the function returns Promise<this> rather than relying on external documentation to know that the constructor produces Promise<this> rather than this.

This is exactly why I recommended static async functions: their type signature does not hide the fact that it returns Promise<this>. This is not the case for the constructor, which TypeScript assumes to (implicitly) return this, not Promise<this>.

Thread Thread
 
fractal profile image
Fractal

Even if it's typed as Awaited<ClassName>?

Thread Thread
 
somedood profile image
Basti Ortiz

As far as I know, TypeScript does not allow the constructor to be directly type-annotated. As of writing, the compiler produces the following error:

Type annotation cannot appear on a constructor declaration.

Collapse
 
yw662 profile image
yw662 • Edited

Please be informed that you are just spamming around.

And your solution is not good at all. You cannot super() in the constructor and you cannot extends the class. And your constructor is not a constructor, which is harmful.

Collapse
 
dandv profile image
Dan Dascalescu

Did you mean that more precisely, you can call super in the constructor; you can't await super() in derived class constructors, but you can extend the class if you don't need to overload the constructor? I've documented this downside in the SO answer.

Can you expand on how the constructor is not a constructor?

Thread Thread
 
yw662 profile image
yw662

A constructor should anyway return the constructed object itself, instead of a promise resolving to the said object, unless the object is the promise.

Thread Thread
 
martijn_scheffer_f5485f4b profile image
Martijn Scheffer

we are discussing creating objects asyncronously, in that case the constructor HAS to return a promise.

Thread Thread
 
somedood profile image
Basti Ortiz

Pedantically, I am on the camp that constructors should never return anything at all. That's just an unexpected quirk of JavaScript that even tsserver trips up on.

Semantically, constructors initialize fields, not return values. Thus, returning a Promise from a constructor is not only awkward, but also unexpected because it returns something aside from the intended class instance (i.e., Promise<Animal> vs. Animal itself).

A static async factory method makes this distinction well: do the async work ahead of time and then invoke the constructor with the now-resolved field initializers.

Collapse
 
martijn_scheffer_f5485f4b profile image
Martijn Scheffer

my preferred solution as well, just return the result of the call to initialize from the constructor (and "this" from initialize)

you either have to document that the class has an asyncronous constructor, or, explain what factory function to call, i know what i prefer, oh and yes, if you include Async in the name of the class itself it will be hard to miss