The Right Way to Load Images and Audio in JavaScript
Chapters10
This chapter introduces loading and managing game assets (images and audio) with promises, explaining how a flexible asset loader keeps the game flow intact while assets load in the background.
Learn a practical, promise-based approach to loading images and audio in JavaScript to keep games responsive and polished.
Summary
Franks Laboratory walks through building a robust asset loader using promises to manage images and audio in a vanilla JavaScript game. The lesson emphasizes not stalling the game loop while assets load, and shows how to wrap image loading in a promise that resolves on load or error. A key pattern is Promise.all for parallel loading, with async/await used to pause initialization until all assets are ready. The instructor refactors the project to include an audio manager alongside the image manager, mirroring the same load/all logic for audio files, and adds defensive error handling so missing assets don’t crash the game. To improve UI and organization, Franks introduces a dedicated UI manager to handle panels, button interactions, and later a timer UI element. The video then demonstrates a live UI enhancement by adding a game timer, styling it with CSS, updating it in the game loop, and ensuring it remains responsive and visually centered. By the end, the starter kit has a clean structure for images, audio, and UI, ready for richer gameplay features. The overall takeaway is a practical blueprint for asset loading that keeps the game flow smooth while providing a foundation for further polish.
Key Takeaways
- Image loading uses a new Promise in the loader to resolve when the image finishes or fails, enabling a non-blocking startup.
- Promise.all is used to wait for all image assets to load in parallel before starting the game, ensuring complete readiness.
- Audio assets follow the same async pattern as images, with an audio manager that loads, tracks, and plays sounds safely.
- UI responsibilities are refactored into a dedicated UI manager, improving separation of concerns and making future UI work easier.
- A live timer UI was added and styled with CSS, then driven by the UIManager to show on-screen game time during play.
- The code includes defensive checks and fallbacks (e.g., resolving on image load error) to prevent a single missing asset from breaking the game.
- The starter kit demonstrates a repeatable pattern: image manager, audio manager, UI manager, all coordinating via promises and async/await for clean startup sequences.
Who Is This For?
Essential viewing for beginner-to-intermediate JavaScript game developers who want reliable asset loading and clean architecture as their projects scale. If you’re rebuilding a game from scratch or refactoring a messy codebase, this guide offers practical, copy-ready patterns.
Notable Quotes
"A promise is basically a way to handle something that finishes later, like loading a file, without breaking the flow of your code."
—Defines the core reason for using promises in asset loading.
"Promise all takes an array of promises and waits for all of them to complete in parallel."
—Explains how to synchronize multiple asset loads efficiently.
"Set timeout is actually spelled like this."
—Illustrates attention to detail when implementing artificial loading delays for testing.
"This allows load all to use promise all to wait for all the images to finish loading in parallel before the game starts."
—Covers the practical outcome of using Promise.all in the loader.
Questions This Video Answers
- How do I implement a scalable asset loader with promises in vanilla JavaScript?
- How does Promise.all help manage multiple asset loads in a game loop?
- What is the role of a UI manager in a JavaScript game architecture and how do I refactor UI code into it?
- How can I add a timer UI to a canvas-based game and keep it centered on the screen?
- What are best practices for handling missing assets without crashing the game?
JavaScriptPromisesAsync/AwaitPromise.allAsset LoadingImage LoaderAudio ManagerUI ManagerGame Starter KitWeb Audio API
Full Transcript
Visual art style, music and sound effects play a huge role in how your game feels. If you want to create any look, any mood, any art style you can imagine, you need to understand how to properly load and manage your assets, images, and audio. One of the reasons I love working with JavaScript is because it gives us powerful tools that make this much easier, like promises. A promise is basically a way to handle something that finishes later, like loading a file, without breaking the flow of your code. And today, we're going to use promises to build a simple, flexible asset loader for both images and sound.
By the end of this lesson, you won't be just loading files. You'll be able to control the entire feel and atmosphere of your game. This video is part of the vanilla JavaScript game development series. You can find the link to the full playlist below. Source code for the project at this stage is available to download in the resources section below. I called it game starter kit part 8. Use it if you hit a bug and want to compare your code to mine or just save it as a checkpoint you can always come back to. I'll be providing source code checkpoints throughout this project to make sure everyone can follow along and nobody gets left behind.
By the end, we'll all have a well ststructured optimized starter kit we can use to build many different games. Inside index html we have main menu and post panel. And down here I will create a loading screen. A div with an ID of loading screen. It will have a shared UI panel class and active class to show and hide it. Same as we do with the other UI panels. Inside I give it a heading that says loading and maybe some text. You can put anything you want here. We could even have a set of phrases that appear randomly.
I might do that later. Okay. So here we have a very simple loading panel. We will handle the loading here inside the init method. But before we do that, we have to adjust our image manager a little bit. I will add promises. If you're finding this project useful, let me know by leaving a like. It really helps the series continue. You can also leave a comment if you have any ideas for systems or features you'd like to see covered next. If you like the assets I used here, check out the link in the description. You will find plenty more in this art style.
Inside the load method, I will return a new promise which will wrap our image loading logic. Notice it has this resolve parameter which is a callback function that we call when the promise completes. I will put all the image loading code inside this promise. I put the resolve function at the end of onload and also at the end of on error. Doing this means that the promise will complete successfully whether the image loads correctly or fails allowing our game to continue either way. We resolve on error instead of rejecting because we have a fallback rendering system that can handle missing images.
So, a failed image load shouldn't stop the entire game from loading. This allows load all to use promise all to wait for all images to finish loading in parallel before the game starts. In JavaScript, a promise is a special object that acts as a placeholder for a result that hasn't happened yet. Because some tasks like fetching data from a server or loading an image take time, JavaScript uses promises to manage that waiting period without freezing your entire program. Notice I'm using async await here in load all. Async await is a syntactic sugar over promises that makes asynchronous code easier to read.
I do it because I need to wait for all images to finish loading before the game continues. The await keyword pauses execution until promise all finishes. And marking the function as async allows me to use a wait inside it and makes load all return a promise that my game class can also await. We will add more image assets here later. This load now returns a promise that resolves when the image loads and also if the image fails to load. The load all method is asynchronous now and it will wait for all the promises we put inside to resolve which in this case means it will wait for all the image assets we put here to load.
Back in gamejs I use the async keyword here before in it and inside I say wait promise all and I cut this image manager load all like this from the constructor and I paste it inside promise all like this. Notice we have async await here. I'm basically saying wait for all these asset managers to finish loading before continuing. Promise all takes an array of promises and waits for all of them to complete in parallel. Once everything is loaded, in it will continue and it will hide the loading screen and show the main menu. Using promises here will make sure the game never starts with missing assets.
We know that we can show and hide UI panels by adding or removing the additional active class. So when the game loads and we enter the main menu, I want to hide the loading screen and I want to show the main menu UI panel. Initially the main menu will not have the active class. Only loading screen will have the active class at first. If I reload my page, we might see a brief loading screen flicker. But we have only one small image asset for the player. So the loading is almost instant. Just for testing, I will make the promise take a little bit longer to resolve so we can see a proper loading screen for a second or two.
And we can really visually verify that all our code is working. Inside load all, I add a new promise that will simply have a set timeout which will resolve after 2 seconds. Set timeout is actually spelled like this. Set timeout is a built-in JavaScript function that schedules a specific piece of code to run only after a designated period of time. In this case, I'm saying create a new promise that will wait for 2 seconds and then it will resolve. And we know that load all will wait for all the awaits it has before it can continue.
So, this artificial delay will keep the loading screen visible long enough for us to verify it's working properly. Of course, we'll remove this delay later once we've confirmed everything works. We know that the init method here is asynchronous and it will pause at this weight line until image manager load all resolves, which now includes that 2cond delay. Only then it will continue to the next line and hide the loading screen and show the main menu. And the game only becomes interactive once everything is ready. Perfect. I reload my page one more time. That works. Nice.
In style CSS, I give the loading screen some extra padding. Save that. Reload the page. Loading. And we are in the main menu. Awesome. This is a clean and simple way to add a loading screen to your game. Of course, we can make the loading screen more interesting later with images or animations. We have the base infrastructure here to handle all of that. Source code for the project at this stage is available to download in the resources section below. I called it game starter kit part 9. Use it if you hit a bug and want to compare your code to mine or just save it as a checkpoint you can always come back to.
I'll be providing source code checkpoints throughout this project to make sure everyone can follow along and nobody gets left behind. By the end, we'll all have a well ststructured optimized starter kit we can use to build many different games. In my project root, I create a folder I call audio. So now we have this images folder to store art assets and audio folder where we will store sound effects or music. I will add just four audio files for now just so we can set up some basic actions. You can use your own sound effects or you can download these in the resources section down below.
We will have a sound effect for button click, button hover, pause and unpause. Inside the JS managers folder, I create a new file. I call audio manager.js. We already have all the infrastructure in place. We will simply do the same thing we are doing here for the image manager. We will have a data structure to hold all the assets. We will have a load method, some kind of a method to get or play that audio when we need it, and a load all method that will make sure all the assets are loaded before the game starts.
I export class audio manager here. The constructor will have an object that will hold all sounds. I will store a name here and a path for each sound effect. And I will keep track of whether it's loaded. The load method will handle loading of these audio assets. The play method will be called from all around the codebase whenever we need to play a specific audio by its name. And we will have an asynchronous load all method that will wait for all our load promises to resolve before it allows the game to run. Exactly the same as we did with image manager.
The load method will take the name of the audio and the path where we store it in our project. The load all method will await all promises, one promise per audio asset. For each asset, we will call this.load from line six. Give it a name of the sound we want to play and its path. So, I want this pause sound and it sits inside the project root inside the audio folder. And the file itself is called pause.mpp3. We also have a separate load call for unpick. So for each audio asset we will call this load method and we are passing it a name and a path.
Inside I create a new audio object and I pass it the path as the source. I take this dot sounds from line three and create an entry in that object with a key value pair where the key is the name of the audio asset and the value is an object with that audio element from line 7 and a loaded flag which will initially be set to false. The audio is not loaded yet. I take that audio and give it an unloaded data handler which runs when the audio file has finished loading and is ready to play.
Inside we access this dot sounds again based on the name of the asset. We access its loaded property and set it to true. I will also console lock audio loaded and use its name here. Then we will resolve the promise. We will also take the audio from line 7 again and add an on error handler. If the audio can't be found or it fails for some other reason, we will console lock audio failed and the name of the audio file and we will resolve the promise anyway to allow the game to continue even if some audio files are missing since audio isn't critical to game play.
The play method will be called whenever we want to play a sound. We pass it the name of the sound we want to play. For example, pause or unpause. We start by first finding that audio by its name inside this dot sounds and we save it as a helper variable I call sound. If we were able to find the sound by the name that was provided and that sound has the loaded flag set to true, we will first rewind that audio to make sure it always plays from the beginning and then we play it. The current time property sets the current playback position in seconds.
So setting it to zero makes sure the audio plays from the beginning every time. I use catch here because audio play returns a promise and modern browsers might reject that promise if the user hasn't interacted with the page like clicking or tapping before the sound tries to trigger. Catching the error like this is a good practice to prevent your application from crashing due to an uncode exception if a browser blocks your audio. Inside I simply console log the name of the audio that couldn't play and the error message inside game.js we always connect all the logic together.
I start by importing the audio manager class from manager audio manager. I instantiate it here right under the image manager. And where do we play these sounds from? It depends on which action we want to trigger that sound. For example, when we are in the main menu and we click the play button, I want the button click sound to play. So down here inside the setup UI method, I add curly braces. And when the play button is clicked, we call this start game and we also take this audio manager and call the custom play method we wrote and we pass it the name of the sound we want to play.
Before we can actually play the sound, we know we have to call the load all method that will prepare the audio files so that they can be used. I will do that here inside the init method. After we call load all on the image manager, I use a comma and I call load all on audio manager. I get some console errors and I can see the issue is on lines 12 and 13 inside audio manager.js. It's because I'm calling resolve, but I didn't turn this into a promise. So, let's fix that. We know that the load method when called will return a promise that will resolve when the audio was loaded or if it failed to load.
So, we have this asynchronous load all method which will wait until all promises are resolved. Each load call for each separate audio file is a separate promise that needs to resolve here or here. If the audio is found and loaded, this will run. Its loaded property is set to true and the promise resolves. Now finally the init method can run. Resolve all promises for all our images and all our audio files. And then the loading screen can be hidden. The main menu is displayed and the game is playable. At this point, when we know the audio assets are loaded, we can use audio manager play from all around game.js to trigger sounds we want to play like we are doing here on line 92.
When I press play, audio will play. In the console, I can see a separate console log for each audio file that was successfully loaded. Here I add curly brackets so that I can execute multiple statements within the body of the arrow function. When resume button is clicked, I still call this resume, but I will also take audio manager and make it play the button click sound. And when quit button is clicked, it will call return to menu. And also play the button click sound like this. Save that. And I test if button click plays when I press play and when I press quit to menu.
Nice. We are successfully playing audio in our game. Down here inside the pause method, I want to play the pause sound. And inside resume, I will play the unpause. Now if I play and repeatedly press the escape key, I toggle pause on and off and the audio plays accordingly. Maybe we also want to add a subtle sound when we hover over a button. I can do that here inside setup UI. I use query selector all to select all button elements on the page as a node list. I then iterate through them using for each to attach a mouse enter event listener to each one.
When a user hovers over any button, the handler triggers the audio manager play method for the button hover sound. Well done. Adding audio to the game goes a long way toward making the experience feel more real and giving the player clear feedback for every action. game starter kit part 10. Use it if you to mine or just save it as a checkpoint. You can always come back to I'll be have a well structured, optimized GameJS is currently handling a lot of things. Let's offload some of its responsibilities. First of all, we are defining the drawing context ctx here and also here.
The only thing GameJS needs the drawing context for is this single color background we are drawing when in the main menu. Let's follow clean separation of concerns. This should be handled by the render system. So inside render systemjs I create a new method called render menu background. Eventually we can add some animations or images but for now it will simply draw the solid color background. So I cut it from here and I paste it in here inside game.js. All we will call is this render system render. I cut it and I paste it here to be called inside the game loop after every update and I completely delete the render method.
Now we can delete this ctx. We don't need a drawing context in this file anymore. Game loop will be updating everything. and then draw in those updated objects using render system render. Render system will need to know the current game state to decide if it should draw the menu or gameplay elements. I will pass it this state from line 19 as the first argument and this player from line 15 as the second argument. I make sure the render method expects state and player as parameters in this order. And inside I say if state is menu we will call render menu background.
Else we will render the grid player and all the other elements our game will have. Save that. We are drawing the solid background in the menu. I press play. We draw the grid and the player. Back in the main menu we draw the solid background again. Perfect. This works. Handling a few buttons and panels directly from GameJS is fine for a small game. But as our project grows, I think at this point before I start adding more UI, it's a good idea to offload UI related responsibilities from Game.js into a dedicated UI manager. In my project folders inside managers, I create a new file called UI manager.
As usual, we export the UI manager class. The constructor takes game as a parameter and inside we assign it to a class property like this. We will have a helper method called setup event listeners. Inside we will basically do what we did here inside setup UI. I copy just this first portion. I delete this line here. I'm simply taking the play button and adding an on click event listener. When the play button is clicked, run the start game method. I copy this. And for resume button, we will run the resume method. These start game and resume methods don't sit on UI manager.
So I have to call them from where they actually are from this.game game like this. They sit on the game class here. We have a reference to it through this. And that's how we can call them from UI manager. One more for quit button. And we call this.game. Return to menu. I also want this code block. So I copy it and paste it here. I wouldn't always play audio from UI manager like this. But in this case, playing the button hover sound is a purely UI related notification. Some of these decisions about where to call things from will always come down to a personal preference.
For each button on mouse enter, we take this.game.audio manager and call play button hover. I call setup event listeners from the class constructor here, which means this will run and apply the listeners automatically when the UI manager is created. Hide all panels is also something UI manager should be handling. Inside I simply find all elements with a class of UI panel and for each panel I remove the active class which we know will hide them. We will also need the ability to show each panel individually. So let's create a helper show panel method that will take panel ID as a parameter.
Whenever we show any panel, we first always call hide all panels. Then we use document getelement by ID and we pass it panel ID. We take its class list and add the active class to show it. Now we will break our code base for a while because it's time to delete hide all panels from game.js since we will replace it with the same method sitting under UI manager instead. I will also delete the entire setup UI method. UI manager will be handling that not game.js JS up here. I will import UI manager from the managers folder.
I instantiate it here the same as we do with the other managers. We can see that it expects game as a parameter. So I pass it this because we are inside the game class. This keyword here represents the entire game object. We know that when we create an instance of a class using the new keyword, the constructor of that class will run automatically and it will call setup event listeners. That means when I create an instance of UI manager, event listeners are automatically applied. Our code is still broken at this point. So let's continue with the refactor.
I delete these two lines here where I hide menu. And instead I call this dot UI manager hide all panels to hide the loading screen and then the new method we just wrote called show panel which expects the ID of the panel we want to show. We want to show main menu. We deleted setup UI. So I delete this line. Down here in start game I call this UI manager hide all panels. This line handles UI directly and we don't want that. We want to handle UI through UI manager. So I replace it with this doi manager.
panel and I pass it pause menu. We know that show panel will first hide all panels and then give the panel we want to show the active class which actually means that up here by showing the main menu panel, it also runs hide all panels inside. So I don't really need this line of code here. I can delete it. I just need to make sure I call all these methods from UI manager. Instead of hiding the post panel when we resume, I call this UI manager hide all panels. Here I replace this with this doi manager show panel and I pass it main menu.
up here. I have to make sure I call this method using this dot like this. Query selector all needs a class name here. So I have to say dot UI panel like this to be able to find all UI panels here. And I'm giving each panel a temporary variable called P. So I have to say P.class list here to remove their active class and hide them. And one more thing inside game.js down here inside return to menu we want to call hide all panels from UI manager. Now the game is working. No errors. We are just missing some sounds.
So inside start game I call this doauudio manager.play button click. I will also call it from return to menu. Now I get a sound when I press the play button to start the game. And when I quit to menu, our game is working and we successfully separated UI related responsibilities into UI manager which will help to stay organized as the code base grows. So let's add a new UI element, a game timer. You will see a timer like this in run-based games like Vampire Survivors or maybe in dayight cycle or racing games. Having a timer is always useful and we can also use it as a win condition later.
Survive enemy waves for 5 minutes or something like that. Inside index html, I create a div with an ID of timer. I set it to zero like this. In styles CSS, I use its ID to target it. I set its position to absolute to remove it from the document flow so it ignores the canvas and can be positioned on top of it. We can see the timer here. I position it 45 pixels from the top of the web page and 50% from the left. To center it exactly in the middle at all times, even as the timer values change, I set transform translate x minus 50%.
which shifts the element left by half of its own width, effectively keeping it horizontally centered over its reference point. I set font size to 40 pixels, color to this neon blue we have been using, font family to this placeholder font for now, and text shadow to electric blue with reduced alpha. font weight of 700 will match our big headings and Z index 1,00 to make sure it's always in front of the canvas. I set display to none here because the timer will be hidden at first since we start in the main menu. Pointer events none will make the mouse ignore this element and ensure clicks are not blocked.
The pointer events none property prevents an element from becoming the target of pointer events like clicks. This allows these events to pass through to whatever elements are positioned behind it. We have this timer element and we styled it. Now we need to handle it with JavaScript. It's a UI element. So we will do that inside UI manager.js. We will need some helper methods. Show timer, hide timer, and update timer. This is a bit of defensive programming. By first saving the element in a variable and only styling it if it exists, we prevent runtime errors if the element is missing or if there is a typo in the ID.
Defensive programming is the practice of writing code that anticipates potential errors or unexpected states, helping our program fail gracefully and remain stable. Defensive programming is commonly used in indie games and production ready code to prevent small mistakes from breaking the game, make debugging easier, and improve overall code reliability. Inside hide timer, we do something very similar, but I set display to none instead. You might notice I'm calling document.getelement by ID each time we update the timer. That's totally fine for now because this operation is really cheap. Later, if we add more UI elements, we could cache these references.
So, we don't have to keep searching the dome inside game.js. When start game runs, we want to show the timer. And when we return to menu, we hide the I play the timer appears. I quit to menu. The timer hides. Perfect. Now we actually have to track the time when the game is running. Inside update timer method. Here we need to somehow calculate minutes and seconds so that the timer can display them. I comment this out for a moment. Inside the game class, I create a new property called this time. Down in game loop, I can delete this console lock.
And if we are playing, I want the timer to accumulate the real time that is passed in. So if this state is playing, I do this time plus equals delta time. Every time we update time, we will call the update timer method and pass it the updated time we want it to display. When we start or restart the game, we set this time to zero here. So update timer is getting the updated time as an argument and I will make sure it expects that parameter here. Inside update timer, we calculate minutes by taking the current time which is passed in seconds and dividing it by 60.
We wrap it in math.f floor to round it down to the nearest whole minute. seconds is the remainder of that same time divided by 60 using the modulo operator. The modulo operator returns the leftover seconds after removing the full minutes. We also wrap this in method floor to ensure we always display a whole number of seconds avoiding any fractional values. The modulo operator returns the remainder of a division between two numbers. For example, 7 modulo 3 equals 1 because 7 / 3 is 2 with a remainder of 1. 2 * 3 is six and there is one remaining to get seven.
It's useful for calculating cycles, wrapping values or extracting seconds from a total time in seconds like we just did in our code here. Then I will get the timer by its ID and I set its text context to a template string where I insert the minutes colon and seconds. Inside gameJS, we already have code here in game loop that is calling update timer every frame and passing it the updated time. So I save the changes and play. And you can see we have a nice animated timer up here. I can wrap the seconds in a string and call pot start with two and zero like this to ensure that singledigit seconds are always displayed with two characters.
For example, five becomes 05. This keeps the timer visually consistent. I can also check how higher values are displayed by setting the start timer to 3,00 seconds here, which will give me 50 minutes displayed like this. The timer works really well. We extracted our UI into a separate UI manager. And we can create all kinds of UI elements here like we just did with the timer depending on what our game needs. In this episode, we set up a solid foundation for handling all our game assets, images, and audio using promises. From here, we can start building a much richer and more polished experience.
Let me know in the comments what we should focus on next.
Get daily recaps from
Franks laboratory
AI-powered summaries delivered to your inbox. Save hours every week while staying fully informed.






