Many websites explain how to implement sleep in TypeScript/JavaScript. It can be implemented by using Promise
and setTimeout
. It looks easy.
function sleep(ms: number): Promise<void> {
return new Promise(resolve => global.setTimeout(resolve, ms));
}
However, don’t you think it’s better if we have more control for the timer?
Haven’t you ever needed to stop the sleep timer when another event is triggered?
Usage of setTimeout
If you want to do something 1 second later, setTimeout
can be used.
global.setTimeout(() => {
console.log("Do what you want here");
}, 1000);
If you have a function and a preparation step is needed, you implement it like the following.
function doSomething(): void{
console.log("Preparation step");
global.setTimeout(() => {
console.log("Do what you want here");
}, 1000);
}
It looks ok at first.. but, what if we need to add an additional process with setTimeout
?
function doSomething(): void {
console.log("Preparation step");
global.setTimeout(() => {
console.log("Do what you want here");
}, 1000);
global.setTimeout(() => {
console.log("Do something 2");
}, 2000);
}
If the process is independent of the first one, this code is ok but if we need to trigger the process after the first process is done, we have to implement it in the following way.
function doSomething(): void {
console.log("Preparation step");
global.setTimeout(() => {
console.log("Do what you want here");
global.setTimeout(() => {
console.log("Do something 2");
}, 1000);
}, 1000);
}
setTimeout
in setTimeout
. It is similar to callback hell.
Creating sleep function with Promise
To make the code clearer, we should use async/await keyword there. The goal of the function looks like this below.
async function doSomething2(): Promise<void> {
console.log("Preparation step");
await sleep(1000);
console.log("Do what you want here");
await sleep(1000);
console.log("Do something2");
}
You can also write in this way.
function doSomething3(): Promise<void> {
console.log("Preparation step");
return sleep(1000)
.then(() => {
console.log("Do what you want here");
return sleep(1000);
})
.then(() => {
console.log("Do something2");
})
}
It is much more readable than the one in the previous section, isn’t it?
To use, async/await keyword, sleep function must return Promise
. The sleep function is implemented in the following way.
function sleep(ms: number): Promise<void> {
return new Promise(resolve => global.setTimeout(resolve, ms));
}
Promise
has resolve
and reject
callback but reject is not necessary here because setTimeout doesn’t throw an error with this code. resolve callback is called when the specified millisecond is elapsed.
Check this article, if you want to know more about Promise
Cancellable sleep
We learned how to use setTimeout
and impelment sleep
function. The next step is to stop the timer somehow.
setTimeout
has a return value. The return data type depends on the system but it is NodeJS.Timeout
on Node.js for example. We can stop the timer.
The following code set the callback but it won’t be triggered because the timer is cleared by clearTimeout
.
const timer = global.setTimeout(() => {
console.log("timeout----");
}, 1000)
global.clearTimeout(timer);
This can be easily implemented where sleep is needed but the sleep function that we defined above doesn’t have reject callback. It means it’s impossible to know the cancellation reason in the caller side.
Let’s improve this.
interface CancellableSleep {
promise: Promise<void>;
cancel(reason?: any): void;
}
function cancellableSleep(ms: number): CancellableSleep {
let timer: NodeJS.Timeout;
let rejectPromise: (reason?: unknown) => void;
const promise = new Promise<void>((resolve, reject) => {
timer = global.setTimeout(() => resolve(), ms);
rejectPromise = reject;
});
return {
cancel: (reason?: unknown) => {
global.clearTimeout(timer);
rejectPromise(reason || new Error("Timeout cancelled"));
},
promise,
};
}
When cancel
function is called, we need to call clearTimeout
and then, call reject callback with the canceled reason.
I implemented it in the following way at first, but IntelliSense shows an error.
// Type 'number' is not assignable to type 'Timeout'
timer = global.setTimeout(resolve, ms);
Usage of cancellable sleep
Let’s see an example of the usage.
The following class tries to connect somewhere. When the connection fails, it retries the connection attempt.
class Connector {
private timeout?: CancellableSleep;
private count = 0;
private readonly maxRetryCount = 3;
public async connect(): Promise<void> {
while (true) {
try {
// try to connect somethere
console.log(`Connection attempt: ${this.count + 1}`);
this.count++;
if (this.count < this.maxRetryCount) {
throw new Error("Connection was not established.");
}
this.count = 0;
return Promise.resolve();
} catch (e) {
if (e instanceof Error) {
console.error(e.message);
}
this.timeout = cancellableSleep(1000);
await this.timeout.promise;
}
}
}
public stop() {
this.timeout?.cancel("Connection is no longer needed.");
this.timeout = undefined;
}
}
As you can see, cancellableSleep
is called in the catch block to retry the connection attempt. It tries to connect 1 second later. It could be 1 minute in production code. There are some cases where we need to stop the timer in order to avoid over retry when the system is interrupted by an event or use interaction.
In this case, a client-side can call the stop method.
The following example stops the timer after 1500 milliseconds.
const connector = new Connector();
connector.connect()
.then(() => {
console.log("Connected");
})
.catch((reason) => {
console.error(`Connection failure: ${reason}`)
});
global.setTimeout(() => connector.stop(), 1500);
// Connection attempt: 1
// Connection was not established.
// Connection attempt: 2
// Connection was not established.
// Connection failure: Connection is no longer needed.
Even if you have many cases to stop the timer for various reasons, error handling can be done in only one place in this way.
Comments