Observables fix broken Promises
Promises may have been made to be broken, but with RxJS Observables you'll experience more manageable asynchronous operations. By making this shift, you not only simplify your code but also address a critical drawback of Promises: their inability to be canceled.
Updated: Apr 10, 2024
Published: Apr 10, 2024
Countdown
Three, two, one, GO!

I'm working on my game, and I'm adding a countdown to start the game automatically if we don't receive any user input for a few seconds. Sounds easy enough, but you'll end up pulling your hair out unless you code it carefully. In JavaScript, we can use setTimeout()
or setInterval()
to handle this, but dismissing them correctly can be more difficult than it sounds.
I decided to write this blog because I got frustrated with why my code didn't work as expected. My countdown was at times being displayed too early, and sometimes two different counters would be displayed at the same time, and it was all a mess. I had code running asynchronously, and not stopping properly, or running when it shouldn't.
If you're working with Phaser, the best 2D game engine for making games in JavaScript (or TypeScript,) you can also combine the update loop with an internal counter, which I think is a better option than setTimeout()
or setInterval()
. The downside is that we clutter our Scene with code that doesn't need to be there. Wouldn't it be better to create a Promise that resolves after a few seconds, and then start the countdown? Sounds like a good idea, but here's the catch: we need to cancel the countdown after receiving input from the user. Promises do not have that functionality built-in, so you need to work around it.
Let's take a look at the following example:
countdown1.js
import { waitForSeconds } from 'utils';
function handleCountdown() {
waitForSeconds(5).then(() => {
startCountdown();
})
}
We can call this function when we start our game. Let's ignore the implementation of waitForSeconds
for the moment. It returns a `Promise` that will resolve after a given number of seconds (5 in the above example.) What do we do if we want to cancel the countdown before it even starts?
One option is to create a condition inside the function to avoid starting the countdown, which is a valid option, but it can get cluttered, and we have to maintain the state correctly.
countdown2.js
import { waitForSeconds } from 'utils';
let didUserNavigate = false;
function handleCountdown() {
waitForSeconds(5).then(() => {
if(didUserNavigate) return; // problem solved, kind of
startCountdown();
})
}
function navigateToNextScene() {
didUserNavigate = true;
}
Instead, I'd convert the `waitForSeconds` function to an Observable (see RxJS) which we can subscribe to. I want to do that for for one important reason: to unsubscribe (cancel it.)
countdown3.js
import { waitForSeconds } from 'utils';
let subscription;
function handleCountdown() {
subscription = waitForSeconds(5).subscribe(() => {
startCountdown();
})
}
function navigateToNextScene() {
subscription.unsubscribe(); // The countdown will never start if the user navigated within 5 seconds
}
At first glance, the last example with an Observable may seem more complicated, and that's true. However, it's tremendously better to work with when you have more complex code, several scenes, and navigate from one scene to another. Unsubscribing from a subscription is far less prone to unexpected async bugs than relying on states, and conditions.
Let's say we want to turn our `startCountdown()`-function to an async task as well. Making it return an Observable makes it easy to follow the same pattern instead of introducing extra states and conditions inside our `handleCountdown()`-function
countdown4.js
import { waitForSeconds } from 'utils';
let waitForSubscription;
let countdownSubscription;
function handleCountdown() {
waitForSubscription = waitForSeconds(5).subscribe(() => {
countdownSubscription = startCountdown().subscribe(() => {
console.log('countdown complete');
});
})
}
function navigateToNextScene() {
waitForSubscription.unsubscribe();
if(countdownSubscription) {
// adding a condtion because it can be undefined
countdownSubscription.unsubscribe();
}
}
I've also provided a CodePen with a complete Phaser example you can play around with.
Don't hesitate to reach out via my contact form, or Twitter (𝕏) @ReitGames, if you need help or have questions. I'm here to assist you on your game development journey.
Consider sharing this post and following me on Twitter (𝕏) @ReitGames. Subscribe to my weekly newsletter for more posts about game development.
Happy coding!