Async Constructors???
Before anyone rushes into the comments section, I must preface this article by emphasizing that there is no standa...
For further actions, you may consider blocking this person and/or reporting abuse
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,
Well, we must first recall that
async
functions always returnPromise
instances. Hence, returning anasync
IIFE from a constructor is the same as if we returned a plainPromise
, 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 aconstructor
), 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.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 preservethis
(is that a big deal?), and the last two points are unclear.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 theasync
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 theconstructor
. Normally, I expect that a class constructor returns an instance of that class. However, usingasync
IIFEs (or chained promises otherwise) goes against this expectation since it does not returnthis
butPromise<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 theconstructor
producesPromise<this>
rather thanthis
.This is exactly why I recommended static
async
functions: their type signature does not hide the fact that it returnsPromise<this>
. This is not the case for theconstructor
, which TypeScript assumes to (implicitly) returnthis
, notPromise<this>
.Even if it's typed as
Awaited<ClassName>
?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: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 cannotextends
the class. And your constructor is not a constructor, which is harmful.Did you mean that more precisely, you can call
super
in the constructor; you can'tawait 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?
A constructor should anyway return the constructed object itself, instead of a promise resolving to the said object, unless the object is the promise.
we are discussing creating objects asyncronously, in that case the constructor HAS to return a promise.
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 intendedclass
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 theconstructor
with the now-resolved field initializers.Exactly. Constructors are even called
__init__
in Python (which is very close to JS regarding OOP design).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
"Though, this pattern does come with one minor downside. The typical constructor provides a standard interface for object initialization. That is, we simply invoke the new operator to construct a new object. However, with factory functions, the caller must be familiar with the proper entry point of the class. Frankly speaking, this is a non-issue."
I may well be misunderstanding or getting something wrong... But this async static function means that we cannot call the class and export an initialised instance to refer to across a whole project.
EG
// spotify-client-class.js
const client = await Spotify.initialize('authorization-code-here');
// A Promise Object
export default client;
// frontend-component.jsx
// will return a Promise, not the Class
import {Spotify}from "./spotify-client-class"
As Spotify.initialize() returns a Promise to refer to the class across other ES modules we need to initialise another instance of the class. Seems counter intuitive for some use-cases. There's also the problem as await is not commonly module scoped (Though this can be configured) and needs to wrapped within a function.
Really interesting post though, thank you.
Ah, yes. That is true. I have not considered this use case. Just to throw some ideas, though, an effective way to set this up correctly (without pure ES modules) is through some kind of an "asynchronous store".
In Svelte, for instance, there are ways to subscribe to an asychronous store so that all subscribed components may react to the (eventually) resolved promise. This works around the limitations of ES modules.
I'm not sure about the equivalent mechanisms in React (or other frameworks). At the top of my head, using some kind of event-based reactivity is how I would work around this issue.
Thanks for pointing this out, though! 🙇♂️
Thank you Basti for responding so quickly.
"In Svelte, for instance, there are ways to subscribe to an asychronous store so that all subscribed components may react to the (eventually) resolved promise. This works around the limitations of ES modules.
I'm not sure about the equivalent mechanisms in React"
Yeah, I can think of a few potential solutions with React (Hook-ify the logic). A really intriguing example/use-case in favour of component based frameworks. Somewhat frustrating that it would require some strange solution to solve with vanilla JS.
im trying to find a solution that allows you to extend a class with async constructor, no luck so far any suggestions ?
one way i came up with is to have the async functionality that you want declared in your base function and then call said async functions in the static initializer function of the class that extends the base class;
However, i don't know if this is a valid solution. it does allow you to invoke super.
I find this pretty clean on its own, to be completely honest. Nice solution! 👍
As others have pointed out in the comments, the biggest issue is the fact that this
async
initializer has to be documented somewhere—as opposed to standardized constructors which require less documentation.Personally speaking, this is a non-issue, especially when the constructor is already
private
orprotected
. That alone should communicate special arrangements at the interface level.Anyway, to reiterate, I believe your solution is totally valid. It's definitely much cleaner than hacking together chained promises in the constructor.
Why still use classes then? If you're using factory functions anyway, you might as well:
const client = await createClient(..)
It seems confusing that you are mixing up two concepts. That would be confusing for other devs in the project (as some classes will not really behave like you would expect).
Also, in the Spotify example, wouldn't it be better to do the fetch first, and pass the result into the constructor? That would avoid this problem all together and there are less dependencies.
Still a creative solution though...
Well, the point of using classes is to encapsulate some unit of state. In the Spotify example, this "unit of state" is the resultant access token (from the code exchange). The static async factory function encapsulates the OAuth-related initialization logic, which does not belong outside the client class. The class, after all, is supposed to provide an abstraction over the Spotify API.
If we were to manually exchange the authorization code for an access token and then pass it into the regular constructor ourselves, then this defeats the whole purpose of the abstraction we were going for. The user does not need to know about the details of the token exchange. That's why we've hidden it behind a factory function.
The factory function only serves as an indirection for the actual constructor. From the caller's perspective, all they have to do is pass in an authorization code; the factory function shall put into the private constructor the received access token on your behalf. All other details are abstracted away by the class.
Once constructed, the class may now use the encapsulated access token for API calls in the future, hence the necessity of classes. The access token has to be stored somewhere, after all.
The alternative—as you said—is to return the access token to the user, and let them pass it into the constructor. Again, that just defeats the purpose of the abstraction.
Factory functions alone will not be able to provide the proper abstractions. If we were to use factory functions everywhere instead of classes, then manual orchestration of return values would be inevitable. At least with classes, we would be able to encapsulate the state (i.e., access tokens) so that future invocations will only use the class fields.
I must emphasize that factory functions are only a mechanism for asynchronous initialization. The construction of the object indirectly occurs in the factory function itself since the resultant parameters are just passed into the actual constructor.
Thus, I don't believe I am confusing topics here. A factory function provides an interface for asynchronous construction, where intermediate resultant values (which the user cannot have known beforehand) are used as parameters in the actual construction of the object itself.
For instance, in the
Spotify
class, the user passes in an authorization code. However, the class constructor requires an access token. The user cannot possibly know this value until theawait
point of the token exchange. The job of the factory function, then, is to pass in the access token to the constructor on the user's behalf.I must concede that this is one of the downsides I've mentioned in the conclusion. There is no standardized naming convention for this pattern, therefore documentation is absolutely necessary.
But, I hope in the future, as more developers become familiar with this "async constructor pattern" I have demonstrated, it will become apparent why a class must be designed that way. Just like how we've all studied the Gang of Four patterns, I hope that this will become the de facto pattern for async constructors because I am frankly tired of reading code that uses the other workarounds with regular constructors—which as you've read in the article does not provide good workarounds. 😕
Thanks! It really hit me like a stroke a genius one day. I have always thought that there must be a better way of doing things. After looking around some open-source projects with asynchronous construction, I noticed the weird workarounds I wrote about in the article. I've always been baffled.
_ Why not just use this? Why not just move the code here?_
And then I realized... Static async factory functions is the solution! And the next thing you know, this article was born. 😂
It's a creative solution for situations where you must use classes. I didn't know about this 'trick' and I would definitely use it if I have to!
But what I'm trying to explain is that classes in JS are just syntactic sugar. What you're essentially doing is modifying the default class functionality to make them behave more like functions. Then why not use normal functions in the first place?
You can do exactly that with (normal) factory functions (example here). Including private/public variables and private/public methods. And the best thing, you don't have to worry about
this
anywhere.It would be a good excercise to rewrite the Spotify example to use factory functions (not classes) and compare them.
Oh, I see! I have slightly misunderstood. If that's the case, I must agree with your point. The
static
is essentially just a namespace based on my usage. To be fair, though, the namespace does make its relationship with theclass
explicit.Indeed! In fact, I have written an article about that years ago as well. The idioms used in that article may be outdated by today's standards, but it's true that factory functions are sufficient for state encapsulation.
From a stylistic perspective, though, I personally prefer using classes for state encapsulation. The class fields make the state more apparent than if we were to use closures and factory functions. Also, classes were designed for this use case, anyway. 😂
There's a cleaner way that sticks with classes and doesn't mix functions in.
Indeed, the async factory functions (methods if anyone prefers) seem to be the cleanest solution to the problem. Everything remains bound to the class itself. Initializing itself is still the responsibility of the class, not some outside function, we don't break the default constructor contract and avoid a ton of internal state control code inside the class. Nice. 🙂
There's an even cleaner solution.
Actually that's not cleaner. You're breaking the constructor contract, which, by definition, should return an instance of the class, not some "random" value. While ECMAScript permits this, a constructor always has a concrete role. Here, this role is broken - the constructor is not a constructor anymore: it's a async factory function which is wrapped in a class. If anything, that's more code to write than a simple function.
Exactly my thoughts here as well! 👌
Great article. Coming from a Java perspective, it might be best to call your async factories
getInstance(...)
instead ofinitialize
orfetchUser
That is true, but the
getInstance
naming convention implies the singleton pattern, which is not the case here. The factory function here constructs a whole new instance (rather than returning a singleton).That's true, but regardless of the singleton pattern, I still find it more readable
Ah, I see. I did point out that this was the unfortunate downside to this pattern: the lack of a standardized interface for object construction. Before, all we had to use was the
new
keyword. With factory functions, we have to adhere to the naming conventions of the project, which may not be the standard everywhere.Cool trick indeed!
You could define one factory by convention, e.g.
await Spotify.create(code)
and apply in your codebase.I'd prefer that to "fetchUser", because you will need more static methods if you want to add behavior.
A single entry point for a class, and then you can have as mathods as necessary.
I totally agree! This is actually how it's done in the Rust ecosystem. Though, instead of
create
, the typical naming convention isnew
. Butcreate
also conveys the same semantics! 👌Another interesting technique is pre-initialization queue which is commonly used in many libraries (e.g. mongoose). Mario Casciaro has done an interesting Twitter thread on this some time ago: twitter.com/mariocasciaro/status/1...
BTW really nice article, keep them coming :)
No need for that. Async constructors can be used directly.
We’ve heard you
I've needed async constructors in the past, but never found a good way of implementing them. A static factory method seem like an elegant and idiomatic solution. Thanks for sharing!
Even more idiomatic is to call async constructors with
await new AsnyConstructor()
.There is one more way of doing an async constructor - Proxy
Typescript:
Javascript:
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
No need for a new pattern. It's perfectly possible to use async constructors natively.
Pretty illuminating, thanks!
First, I was baffled for when I would need this.
But then I saw the async factory function and remembered that I wrote this multiple times, I just didn't call it an async constructor, haha.
I also like the first idea with returning a promise. I think Ember.js did this in many places.
have been using the private constructor approach, but there are some occasions you would need a real async constructor, exp., a custom element.
Yep. And it's easy to use an async constructor.
But a custom element is where you need to super().
For F*cks sake Dan,
get over it, you don't have to comment a gazillion times "BUT MY SOLUTION IS BETTER!" ...
Arguments have been exchanged, let the readers decide for themselves....
Why # in attributes ? Never seen that before ?
Oh, the
#
are for declaring private instance fields. They're a really neat feature if you want to keep your class variables secret.sorry but, is that javascript ?, private ? string ?
looks more like typescript :)
Yes, it is! I believe I have disclaimed that throughout the article, but the async factory pattern is more important than the syntax itself.
Feel free to send a message request on Twitter! 👍