You're here because you have a question : how do I await a Class constructor? How do I make the constructor Async???
And the answer, I'm sorry, is that you literally can't. A constructor for a class in Javascript must be synchronous. However, there are alternatives in implementations that can help you do what you want. Let's explore it!
You probably want this because you need to initialize something in the constructor and have it available in any call you make to the class. A good example of this would be connecting to a database and waiting for that connection to be established before accepting any queries.
The pattern for this is really simple! Let's start with an example database connector, using the sqlite
module on npm:
import sqlite3 from 'sqlite3';
import { open } from 'sqlite';
To open this database, you need to await the open
function. I asked my friendly AI code helper to come up with a "DBConnector" class and it actually got the common pattern right - the one you'll see everywhere:
class DBConnector {
constructor() {
this.db = null;
}
async connect() {
this.db = await open({
filename: './database.sqlite',
driver: sqlite3.Database,
});
}
async disconnect() {
await this.db.close();
}
async query(sql, params) {
return this.db.all(sql, params);
}
}
// exporting a singleton so there's only one connection
export default new DBConnector();
You'd call this simply by importing it and then awaiting its calls:
import db from './dbconnector.js';
await db.connect();
await db.query('GET * FROM test');
So now, the problem here of course, is that not only do you need to manually call myDB.connect()
in order to start the connection, you also can't guarantee that the query
call will work, say, if a different file does a query as your main file is connecting.
Sure, the main file can await db.connect();
, but anything else importing this module won't have any way to do that. And you might think, "ok but I can call await db.connect();
in other files too, right?" and you can... but that will reconnect to the database every time, which can be slow depending on what you're using.
The Deferred Pattern
The pattern that I came up with involves a little bit more complexity, but it remains simple and ensures that every little piece of code - and yourself - are all happy. And I did in fact come up with it on my own, even if it's actually known to others. They're called "Deferred" promises.
Here's how it works.
// still have our imports
import sqlite3 from 'sqlite3';
import { open } from 'sqlite';
// Create the class
class DBConnector {
// We'll need these private properties:
#db;
#defer;
#resolve;
#reject;
// Then we make our constructor:
constructor() {
// We create a new promise and store its resolve and reject
// functions in the class instance properties.
this.#defer = new Promise((res, rej) => {
// this is the magic, right here, basically.
this.#resolve = res;
this.#reject = rej;
});
// So now, this.#defer is a promise! We can await it in other methods.
// Now we call our this.connect *internally* and automatically.
this.connect();
}
async connect() {
try {
this.#db = await open({
filename: `./database.sqlite`,
driver: sqlite3.Database,
});
// Now that we resolve the promise, any other method that awaits
// this.#defer will continue executing.
this.#resolve();
} catch (err) {
// in case of error we can of course reject the promise
// any code using it would then throw an error.
this.#reject(err);
}
}
async disconnect() {
// on any action, we just await this.#defer
await this.#defer;
await this.#db.close();
}
async query(sql, params) {
// Even in queries, it works perfectly fine!
await this.#defer;
// Here we KNOW that this.#db is already initialized.
return this.#db.all(sql, params);
}
}
export default new DBConnector();
Now ain't that a charming little pattern? It is, of course, more code that a basic example, but personally, I think it definitely raises the bar on boilerplate code for classes that are mostly based on async methods.
Let's see how to use it!
import db from './dbconnector.js';
// it's already initialized... just use it!
await db.query('GET * FROM test');
// and it just works :D
Bonus: I made a library!
I'm using this pattern so often that in the end, I decided to make a really quick & dirty library and publish it on NPM. It's called deferrals and it's pretty trivial to use (it was pretty trivial to write, too, once I knew the pattern).
Let's remake the above example using deferrals
instead.
import sqlite3 from "sqlite3";
import { open } from "sqlite";
// We import the library, of course.
import { makeDefer, waitForDefer, resolveDefer, rejectDefer } from 'deferrals';
class DBConnector {
#db;
// We don't need the private properties anymore.
constructor() {
// Our constructor is a bit simpler.
makeDefer('db');
// We still call our connect method.
this.connect();
}
async connect() {
try {
this.#db = await open({
filename: `./database.sqlite`,
driver: sqlite3.Database,
});
// We resolve the defer here
resolveDefer('db');
} catch (err) {
// or we reject it here.
rejectDefer(err);
}
}
async disconnect() {
// We still need to await something: the defer.
await waitForDefer('db');
await this.#db.close();
}
async query(sql, params) {
await waitForDefer('db');
return this.#db.all(sql, params);
}
}
export default new DBConnector();
The 'db'
string is basically any string name that you want to give to the defer, and you can create as many as you like. Obviously you don't need this library, but personally I think it's quite nice, y'know?
I hope you learned a really useful pattern here, and I'm glad to have been part of your learning journey today <3
Top comments (0)