Anticipated FAQ:
initMethod
because the initMethod
is a hook that needs to be called at a certain time.Sorry, it's just that I saw some similar questions marked as "duplicate", so I wanted to put these FAQ at the top.
My issue is the following race condition:
class Service {
private x: string | null = null;
initMethod() {
this.x = 'hello';
}
async methodA() {
return this.x.length;
}
}
const service = new Service();
setTimeout(() => service.initMethod(), 1000);
service.methodA().catch(console.log);
TypeError: Cannot read properties of null (reading 'length')
at Service.methodA (<anonymous>:15:19)
at <anonymous>:20:9
at dn (<anonymous>:16:5449)
I need something like a Promise whose settled value can be set from another part of the code. Something like:
class Service {
private x: SettablePromise<string> = new SettablePromise();
initMethod() {
this.x.set('hello');
}
async methodA() {
return (await this.x).length;
}
}
const service = new Service();
setTimeout(() => service.initMethod(), 1000);
service.methodA().catch(console.log);
The best I can come up with is to make a class that polls a value until it turns non-null. I'm hoping there's something smarter. I don't want to have to fine-tune a poll interval.
Sorry for the initial confusing example. The race condition is that methodA
can be called before initMethod
.
There was also an unnecessary async
in the initMethod
. I just made it async
because the real method it was based on is async
.
In the following example, you can run the init
before or after the async method call. Either will work -
const s = new Service()
// init
s.init()
// then query
s.query("SELECT * FROM evil").then(console.log)
const s = new Service()
// query first
s.query("SELECT * FROM evil").then(console.log)
// then init
s.init()
deferred
The solution begins with a generic deferred
value that allows us to externally resolve or reject a promise -
function deferred() {
let resolve, reject
const promise = new Promise((res,rej) => {
resolve = res
reject = rej
})
return { promise, resolve, reject }
}
service
Now we will write Service
which has a resource
deferred value. The init
method will resolve the resource at some point in time. The asynchronous method query
will await the resource before it proceeds -
class Service {
resource = deferred() // deferred resource
async init() {
this.resource.resolve(await connect()) // resolve resource
}
async query(input) {
const db = await this.resource.promise // await resource
return db.query(input)
}
}
connect
This is just some example operation that we run in the init
method. It returns an object with a query
method that mocks a database call -
async function connect() {
await sleep(2000) // fake delay
return {
query: (input) => {
console.log("running query:", input)
return ["hello", "world"] // mock data result
}
}
}
function sleep(ms) {
return new Promise(r => setTimeout(r, ms))
}
demo
function deferred() {
let resolve, reject
const promise = new Promise((res,rej) => {
resolve = res
reject = rej
})
return { promise, resolve, reject }
}
class Service {
resource = deferred()
async init() {
this.resource.resolve(await connect())
}
async query(input) {
const db = await this.resource.promise
return db.query(input)
}
}
async function connect() {
await sleep(2000)
return {
query: (input) => {
console.log("running query:", input)
return ["hello", "world"]
}
}
}
function sleep(ms) {
return new Promise(r => setTimeout(r, ms))
}
const s = new Service()
s.query("SELECT * FROM evil").then(console.log)
s.init()
always handle rejections
If init
fails to connect to the database, we need to reflect that with the resource, otherwise our program will hang indefinitely -
class Service {
resource = deferred()
async init() {
try {
const db = await timeout(connect(), 5000) // timeout
this.resource.resolve(db)
}
catch (err) {
this.resource.reject(err) // reject error
}
}
async query(input) {
const db = await timeout(this.resource.promise, 5000) // timeout
return db.query(input)
}
}
function timeout(p, ms) {
return Promise.race([
p,
sleep(ms).then(() => { throw Error("timeout") }),
])
}