Been working on a browser-based RPG and I keep getting tempted to use async/await for game logic stuff — loading assets mid-session, dialogue sequences, timed cutscene beats. And every time I do it I regret at least part of the decision.
The obvious trap everyone hits first: async functions don't pause your game loop, they schedule microtasks. So if you do await showDialogue() inside your update function thinking the loop will "wait," it absolutely will not. The loop keeps ticking. Game state diverges from whatever your async operation expected. Fun debugging session.
The pattern I landed on for dialogue and cutscenes is a state machine that suspends normal update logic rather than trying to async/await through it:
// instead of this (will ruin your day)
async function update() {
if (triggerDialogue) await showDialogue();
movePlayer(); // still runs immediately
}
// do this instead
function update() {
if (gameState === 'DIALOGUE') {
dialogueSystem.tick();
return;
}
movePlayer();
}
For actual async work — loading texture atlases mid-session, fetching save data — Promises are fine. But I queue resolved results and apply them at the start of the next frame so they never land mid-update. It works but feels clunky as hell.
What actually caught me off guard: requestAnimationFrame callbacks and microtask queue ordering can behave weird depending on what triggered the rAF. Had a bug where a Promise resolved between two rAF callbacks and state was briefly inconsistent in a way that only showed up under load. Took embarrassingly long to track down.

Is the real answer just "keep async entirely out of game logic and handle everything at frame boundaries"? Or does someone have a cleaner pattern for this, especially save/load, that doesn't fight the deterministic loop?