DEV Community

Cover image for The Proper Way to Write Async Constructors in JavaScript
Basti Ortiz
Basti Ortiz

Posted on • Edited on

The Proper Way to Write Async Constructors in JavaScript

Async Constructors???

Before anyone rushes into the comments section, I must preface this article by emphasizing that there is no standardized way to write asynchronous constructors in JavaScript yet. However, for the time being, there are some workarounds. Some of them are good... but most of them are quite unidiomatic (to say the least).

In this article, we will discuss the limitations of the various ways we've attempted to emulate async constructors. Once we've established the shortcomings, I will demonstrate what I have found to be the proper async constructor pattern in JavaScript.

A Quick Crash Course about constructor

Before ES6, there was no concept of classes in the language specification. Instead, JavaScript "constructors" were simply plain old functions with a close relationship with this and prototype. When classes finally arrived, the constructor was (more or less) syntactic sugar over the plain old constructor functions.

However, this does have the consequence that the constructor inherits some of the quirky behavior and semantics of the old constructors. Most notably, returning a non-primitive value from a constructor returns that value instead of the constructed this object.

Suppose we have a Person class with a private string field name:

class Person {
    #name: string;
    constructor(name: string) {
        this.#name = name;
    }
}
Enter fullscreen mode Exit fullscreen mode

Since the constructor implicitly returns undefined (which is a primitive value), then new Person returns the newly constructed this object. However, if we were to return an object literal, then we would no longer have access to the this object unless we somehow include it inside the object literal.

class Person {
    #name: string;
    constructor(name: string) {
        this.#name = name;

        // This discards the `this` object!
        return { hello: 'world' };
    }
}

// This leads to a rather silly effect...
const maybePerson = new Person('Some Dood');
console.log(maybePerson instanceof Person); // false
Enter fullscreen mode Exit fullscreen mode

If we intend to preserve the this object, we may do so as follows:

class Person {
    #name: string;
    constructor(name: string) {
        this.#name = name;

        // This preserves the `this` object.
        return { hello: 'world', inner: this };
    }

    get name() { return this.#name; }
}

// This leads to another funny effect...
const maybePerson = new Person('Some Dood');
console.log(maybePerson instanceof Person);       // false
console.log(maybePerson.inner instanceof Person); // true
console.log(maybePerson.name);                    // undefined
console.log(maybePerson.inner.name);              // 'Some Dood'
Enter fullscreen mode Exit fullscreen mode

Workaround #1: Deferred Initialization

Sooo... if it's possible to override the return type of a constructor, then wouldn't it be possible to return a Promise from inside the constructor?

As a matter of fact, yes! A Promise instance is indeed a non-primitive value after all. Therefore, the constructor will return that instead of this.

class Person {
    #name: string;
    constructor() {
        // Here, we simulate an asynchronous task
        // that eventually resolves to a name...
        return Promise.resolve('Some Dood')
            .then(name => {
                // NOTE: It is crucial that we use arrow
                // functions here so that we may preserve
                // the `this` context.
                this.#name = name;
                return this; 
            });
    }
}
Enter fullscreen mode Exit fullscreen mode
// We overrode the `constructor` to return a `Promise`!
const pending = new Person;
console.log(pending instanceof Promise); // true
console.log(pending instanceof Person);  // false

// We then `await` the result...
const person = await pending;
console.log(person instanceof Promise); // false
console.log(person instanceof Person);  // true

// Alternatively, we may directly `await`...
const anotherPerson = await new Person;
console.log(anotherPerson instanceof Promise); // false
console.log(anotherPerson instanceof Person);  // true
Enter fullscreen mode Exit fullscreen mode

We have essentially implemented deferred initialization! Although this workaround emulates an async constructor, it does come with significant drawbacks:

  • Does not support async-await syntax.
  • Requires manual chaining of promises.
  • Requires careful preservation of this context.1
  • Violates many assumptions made by type inference providers.2
  • Overrides the default behavior of constructor, which is unexpected and unidiomatic.

Workaround #2: Defensive Programming

Since overriding the constructor is semantically problematic, perhaps we should employ some "state-machine-esque" wrapper, where the constructor is merely an "entry point" into the state machine. We would then require the user to invoke other "lifecycle methods" to fully initialize the class.

class Person {
    /**
     * Observe that the field may now be `undefined`.
     * This encodes the "pending" state at the type-level.
     */
    this.#name: string | null;

    /** Here, we cache the ID for later usage. */
    this.#id: number;

    /**
     * The `constructor` merely constructs the initial state
     * of the state machine. The lifecycle methods below will
     * drive the state transitions forward until the class is
     * fully initialized.
     */
    constructor(id: number) {
        this.#name = null;
        this.#id = id;
    }

    /**
     * Observe that this extra step allows us to drive the
     * state machine forward. In doing so, we overwrite the
     * temporary state.
     *
     * Do note, however, that nothing prevents the caller from
     * violating the lifecycle interface. That is, the caller
     * may invoke `Person#initialize` as many times as they please.
     * For this class, the consequences are trivial, but this is not
     * always true for most cases.
     */
    async initialize() {
        const db = await initializeDatabase();
        const data = await db.fetchUser(this.#id);
        const result = await doSomeMoreWork(data);
        this.#name = await result.text();
    }

    /**
     * Also note that since the `name` field may be `undefined`
     * at certain points of the program, the type system cannot
     * guarantee its existence. Thus, we must employ some defensive
     * programming techniques and assertions to uphold invariants.
     */
    doSomethingWithName() {
        if (!this.#name) throw new Error('not yet initialized');
        // ...
    }

    /**
     * Note that the getter may return `undefined` with respect
     * to pending initialization. Alternatively, we may `throw`
     * an exception when the `Person` is not yet initialized,
     * but this is a heavy-handed approach.
     */
    get name() { return this.#name; }
}
Enter fullscreen mode Exit fullscreen mode
// From the caller's perspective, we just have to remember
// to invoke the `initialize` lifecycle method after construction.
const person = new Person(1234567890);
await person.initialize();
console.assert(person.name);
Enter fullscreen mode Exit fullscreen mode

Just like the previous workaround, this also comes with some notable drawbacks:

  • Produces verbose initialization at the call site.
  • Requires the caller to be familiar with the lifecycle semantics and internals of the class.
  • Necessitates extensive documentation on how to properly initialize and use the class.
  • Involves runtime validation of lifecycle invariants.
  • Makes the interface less maintainable, less ergonomic, and more prone to misuse.

The Solution: Static Async Factory Functions!

Rather amusingly, the best async constructor is no constructor at all!

In the first workaround, I hinted at how the constructor may return arbitrary non-primitive objects. This allows us to wrap the this object inside a Promise to accommodate deferred initialization.

Everything falls apart, however, because in doing so, we violate the typical semantics of a constructor (even if it's permissible by the Standard).

So... why don't we just use a regular function instead?

Indeed, this is the solution! We simply stick with the functional roots of JavaScript. Instead of delegating async work to a constructor, we indirectly invoke the constructor via some async static factory function.3 In practice:

class Person {
    #name: string;

    /**
     * NOTE: The constructor is now `private`.
     * This is totally optional if we intend
     * to prevent outsiders from invoking the
     * constructor directly.
     *
     * It must be noted that as of writing, private
     * constructors are a TypeScript-exclusive feature.
     * For the meantime, the JavaScript-compatible equivalent
     * is the @private annotation from JSDoc, which should
     * be enforced by most language servers. See the annotation
     * below for example:
     *
     * @private
     */
    private constructor(name: string) {
        this.#name = name;
    }

    /**
     * This static factory function now serves as
     * the user-facing constructor for this class.
     * It indirectly invokes the `constructor` in
     * the end, which allows us to leverage the
     * `async`-`await` syntax before finally passing
     * in the "ready" data to the `constructor`.
     */
    static async fetchUser(id: number) {
        // Perform `async` stuff here...
        const db = await initializeDatabase();
        const data = await db.fetchUser(id);
        const result = await doSomeMoreWork(data);
        const name = await result.text();

        // Invoke the private constructor...
        return new Person(name);
    }
}
Enter fullscreen mode Exit fullscreen mode
// From the caller's perspective...
const person = await Person.fetchUser(1234567890);
console.log(person instanceof Person); // true
Enter fullscreen mode Exit fullscreen mode

Given my contrived example, this pattern may not seem powerful at first. But, when applied to real-world constructs such as database connections, user sessions, API clients, protocol handshakes, and other asynchronous workloads, it quickly becomes apparent how this pattern is much more scalable and idiomatic than the workarounds discussed previously.

In Practice

Suppose we wanted to write a client for the Spotify Web API, which requires an access token. In accordance with the OAuth 2.0 protocol, we must first attain an authorization code and exchange it for an access token.

Let us assume we already have the authorization code present. Using factory functions, it is possible to initialize the client using the authorization code as a parameter.

const TOKEN_ENDPOINT = 'https://accounts.spotify.com/api/token';

class Spotify {
    #access: string;
    #refresh: string;

    /**
     * Once again, we set the `constructor` to be private.
     * This ensures that all consumers of this class will use
     * the factory function as the entry point.
     */
    private constructor(accessToken: string, refreshToken: string) {
        this.#access = accessToken;
        this.#refresh = refreshToken;
    }

    /**
     * Exchanges the authorization code for an access token.
     * @param code - The authorization code from Spotify.
     */
    static async initialize(code: string) {
        const response = await fetch(TOKEN_ENDPOINT, {
            method: 'POST',
            body: new URLSearchParams({
                code,
                grant_type: 'authorization_code',
                client_id: env.SPOTIFY_ID,
                client_secret: env.SPOTIFY_SECRET,
                redirect_uri: env.OAUTH_REDIRECT,
            }),
        });
        const { access_token, refresh_token } = await response.json();
        return new Spotify(access_token, refresh_token);
    }
}
Enter fullscreen mode Exit fullscreen mode
// From the caller's perspective...
const client = await Spotify.initialize('authorization-code-here');
console.assert(client instanceof Spotify);
Enter fullscreen mode Exit fullscreen mode

Observe that unlike in the second workaround, the existence of the access token is enforced at the type-level. There is no need for state-machine-esque validations and assertions. We may rest assured that when we implement the methods of the Spotify class, the access token field is correct by construction—no strings attached!

Conclusion

The static async factory function pattern allows us to emulate asynchronous constructors in JavaScript. At the core of this pattern is the indirect invocation of constructor. The indirection enforces that any parameters passed into the constructor are ready and correct at the type-level. It is quite literally deferred initialization plus one level of indirection.

This pattern also addresses all of the flaws of previous workarounds.

  • Allows async-await syntax.
  • Provides an ergonomic entry point into the interface.
  • Enforces correctness by construction (via type inference).
  • Does NOT require knowledge of lifecycles and class internals.

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. A quick skim of the documentation should be sufficient in nudging the user into the right direction.4 Just to be extra careful, invoking a private constructor should emit a compiler/runtime error that informs the user to initialize the class using the provided static factory function.

In summary, among all the workarounds, factory functions are the most idiomatic, flexible, and non-intrusive. We should avoid delegating async work onto the constructor because it was never designed for that use case. Furthermore, we should avoid state machines and intricate lifecycles because they are too cumbersome to deal with. Instead, we should embrace JavaScript's functional roots and use factory functions.


  1. In the code example, this was done through arrow functions. Since arrow functions do not have a this binding, they inherit the this binding of its enclosing scope. 

  2. Namely, the TypeScript language server incorrectly infers new Person to be of type Person rather than type Promise<Person>. This, of course, is not exactly a bug because the constructor was never meant to be used as such. 

  3. Roughly speaking, a factory function is a function that returns a new object. Before the introduction of classes, factory functions typically returned object literals. Aside from the traditional constructor functions, this was the no-strings-attached way to parameterize object literals. 

  4. In fact, this is how it's done in the Rust ecosystem. In Rust, there is no such thing as a constructor. The de facto way of initializing objects is either directly through struct expressions (i.e., object literals) or indirectly through factory functions. Yes, factory functions! 

Top comments (51)

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.

Thread Thread
 
ivankleshnin profile image
Ivan Kleshnin

Exactly. Constructors are even called __init__ in Python (which is very close to JS regarding OOP design).

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

Collapse
 
cloudbop profile image
CloudBop

"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.

Collapse
 
somedood profile image
Basti Ortiz

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! 🙇‍♂️

Collapse
 
cloudbop profile image
CloudBop

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.

Collapse
 
rasguy92 profile image
Fernando Trouw • Edited

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.

class Base {
  #something;
  constructor(something){
    this.#something = something;
  }
  static async fetchSomething(){
    const something = await fetchYourThing();
    return something;
  }
  get something(){
    return this.#something;
  }
}

class Feature extends Base {
  constructor(something){
    super(something)
  }
  static async initializer(){
    const something = await this.fetchSomething();
    return new Feature(something);
  }
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
somedood profile image
Basti Ortiz

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 or protected. 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.

Collapse
 
reinoute profile image
Reinout • Edited

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...

Collapse
 
somedood profile image
Basti Ortiz

Why still use classes then?

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.

Wouldn't it be better to do the fetch first, and then pass the result into the constructor?

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.

It seems you are mixing up two concepts.

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 the await 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.

That would be confusing for other devs in the project.

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. 😕

Still a creative solution, though...

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. 😂

Collapse
 
reinoute profile image
Reinout

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?

Well, the point of using classes is to encapsulate some unit of state.

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.

Thread Thread
 
somedood profile image
Basti Ortiz

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?

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 the class explicit.

You can do exactly that with (normal) factory functions.

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. 😂

Collapse
 
dandv profile image
Dan Dascalescu

There's a cleaner way that sticks with classes and doesn't mix functions in.

Collapse
 
tehmoros profile image
Piotr "MoroS" Mrożek

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. 🙂

Collapse
 
dandv profile image
Dan Dascalescu

There's an even cleaner solution.

Collapse
 
tehmoros profile image
Piotr "MoroS" Mrożek

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.

Thread Thread
 
somedood profile image
Basti Ortiz

Exactly my thoughts here as well! 👌

Collapse
 
itays123 profile image
Itay Schechner

Great article. Coming from a Java perspective, it might be best to call your async factories getInstance(...) instead of initialize or fetchUser

Collapse
 
somedood profile image
Basti Ortiz

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).

Collapse
 
itays123 profile image
Itay Schechner

That's true, but regardless of the singleton pattern, I still find it more readable

Thread Thread
 
somedood profile image
Basti Ortiz

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.

Thread Thread
 
darlanalves profile image
Darlan Alves

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.

Thread Thread
 
somedood profile image
Basti Ortiz

I totally agree! This is actually how it's done in the Rust ecosystem. Though, instead of create, the typical naming convention is new. But create also conveys the same semantics! 👌

Collapse
 
loige profile image
Luciano Mammino

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 :)

Collapse
 
dandv profile image
Dan Dascalescu
Collapse
 
gka profile image
Gregor Aisch

We’ve heard you

Collapse
 
momander profile image
Martin Omander

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!

Collapse
 
dandv profile image
Dan Dascalescu
Collapse
 
bakasura980 profile image
bakasura980

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()
Enter fullscreen mode Exit fullscreen mode

import A from './a.ts'

(async () => {
    const a = await new A('Some iNFO')
})()

Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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

Collapse
 
dandv profile image
Dan Dascalescu

No need for a new pattern. It's perfectly possible to use async constructors natively.

Collapse
 
kayis profile image
K

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.