Why Does My onload Function Return ‘undefined’ in JavaScript?

Problem
When I ask an onload function to return a variable, it responds with undefined.

Solution
Use a callback, silly! For onload and ready functions, ditch your return command for a callback function!

Explanation
I was initially going to title this post JavaScript Callbacks and Asynchronous Responses, but I didn’t want to give the false impression I was a real JavaScript developer or something. Plus, that’s not what anyone searches for. Today I ran into my first problem where I needed for a function to wait to return a value, but it wouldn’t return anything but undefined. And let me tell you—if I knew what the heck an asynchronous response was, I wouldn’t even be having a problem in the first place.

I was working on a photographer’s website, developing some code that would pre-load and size images dynamically and quickly. I’m no stranger to jQuery’s $.ajax and $.load() functions, but in this scenario, I was unsatisfied with the response times in using “lazy” loading methods. I needed better performance. Rather than build a controller page in PHP for JavaScript to query, I used data attributes to pre-load the images directly, and do all my parsing in JavaScript rather than take my usual, comfortable route of parsing in PHP. But all that is besides the point. The point is: I needed the fastest, simplest solution possible to load a remote image, but waiting for a response gummed up the entire page. I knew my function was getting the remote image successfully, but it took so long to finish executing that the line of code that requested it in the first place had given up waiting for a response.

Here’s a simplified version of the code I was using:

function imageTemplate(url) {
		var size = imageSize(url);
		var html = '<img src="' + url + '" style="width:' + size.width + ';height:' + size.height + ';">';
		return html;
	}

	function imageSize(url) {
		var response = {};
		var img = new Image();
		img.onload = function() {
			var x = img.width + 'px';
			var y = img.height + 'px';
			var z = y/x;
			response = {width:x,height:y};
			return response;
		}
		img.src = url;
	}

	var imgHTML = imageTemplate('image.jpg');
	console.log(imgHTML);

	// Outputs: <img src="image.jpg" style="width:undefined;height:undefined">

Pretty simple, right? The primary function here is imageTemplate, which takes an input (url), generates the image HTML code and uses the secondary function, imageSize, to fill in the missing width and height. This code works full well—if you change return response; to console.log(response);, you’ll see it doesn’t log undefined but returns the exact values it should.

If that’s true, why doesn’t return work?
When JavaScript parses through the code line-by-line, it sees the onload function and passes over it, waiting until all the other code has been processed to execute it at the end. JavaScript knows it doesn’t have that information in memory—it has to retrieve that information elsewhere, and wait for it to finish. This method is extremely efficient at delivering quick page load times, and it makes sense when you think about it like this: finish the quick tasks first; save the slow tasks for the end. Make no mistake—JavaScript does execute all the code within the onload block. But when it comes time to make a “quick” function wait for a “slow” function to finish, you have a problem.

Imagine if JavaScript was a company. There’s one really big whiteboard in the center of the office, accessible to everyone and viewable to everyone. It works out that whenever employees need to share information with each other, they only use the whiteboard because it’s so prominent. Saves on post-its and emails and such. Anyway, in the middle of a usual day, employee Finn gets recruited to make a coffee run. Likewise, employee Jake gets recruited to write down Finn’s coffee run price into a ledger. When Finn and Jake begin their tasks, Finn heads out the building and down the street while Jake immediately checks the whiteboard for the price. Because of this, Jake writes down I didn’t see a price in the log book before Finn even made it back inside the building. Finn does write the price on the whiteboard as soon as he gets back, but Jake has already written something down in the log book, and being the lazy (or efficient?) employe he is, he doesn’t want to get back up from his desk again.

When it comes time to make a “quick” function wait for a “slow” function to finish, you have a problem.

What kind of company is this? Do these people even talk to each other? Are all the employees mute? How can we make one task dependent on the completion of another? How long does it even take to make a coffee run?

The answer to all of the above questions is: a callback function. Functionally, this means ditching your return values for function placeholders. For example, instead of

function getCoffeePrice() {
		var coffee = new CoffeeRun();
		coffee.onload = function() {
			return price;
		}
	}

	var price = getCoffeePrice();
	console.log(price);

let’s try

function getCoffeePrice(callback) {
		var coffee = new CoffeeRun();
		coffee.onload = function(price) {
			callback(price);
		}
	}

	getCoffeePrice(function(price) {
		console.log(price)
	});

Notice here return is completely missing, and replaced instead with a function (callback) that’s the exact same reference to one of its parameters (note that it doesn’t actually have to be called callback; whatever you name it only has to match the parameter reference above). Also note that we moved the price variable inside the callback function, because now we need that variable to receive the output from the new parameter in getCoffeePrice. As for explaining the intricate scientific details as to why this method works, I would absolutely love to. But unfortunately, I don’t know why.

Wasn’t that the title of this post?
… um …

Taking what we learned about callbacks and applying it to my image loading function from earlier, this is what it becomes:

function imageTemplate(url, callback) {
		imageSize(url, function(size) {
			var html = '<img src="' + url + '" style="width:' + size.width + ';height:' + size.height + ';">';
			callback(html);
		});
	}

	function imageSize(url, callback) {
		var response = {};
		var img = new Image();
		img.onload = function() {
			var x = img.width + 'px';
			var y = img.height + 'px';
			var z = y/x;
			response = {width:x,height:y};
			if(callback) callback(response);
		}
		img.src = url;
	}

	imageTemplate('image.jpg', function(response) {
		console.log(response);
	});

	// Outputs: <img src="image.jpg" style="width:400px;height:300px;">

No undefined responses! Note that here, too, we’ve replaced return response with callback(response). But notice this goes one step further: imageSize now has 2 parameters: one to insert a url into; one to feed a callback to. If you thought parameters were only for input, you’re vastly underutilizing them. Here, you can think of it as url being the input parameter, and callback being the output parameter.

Also notice that on line 2, you have an uninitialized variable—size—inside the callback function. Why? This is an arbitrary variable, and it helps to think of it as receiving the data from an output parameter. Because we took the return code away, we can’t set var size = imageSize(url) anymore. But we can take that same variable, and move it over to capture the callback output that moved to the second parameter of the function. Think of it this way: if we took it away and had ... url, function() { ... instead, where would the output response from imageSize go?

If you thought function parameters were only for input, you’re vastly underutilizing them.

Isn’t that two callbacks? Isn’t that confusing?
You now have 2 callbacks to accommodate the 2 functions involved, even though there’s only 1 onload function. I’ll admit, adding asynchronous code in one place can add a chain effect of complexity, and I’m not skilled enough of a JavaScript developer to give advice on managing entire applications. But I can say that understanding callbacks in JavaScript is a simple, foundational starting point to tackling problems with asynchronous returns.

Tips

  • Make callbacks optional. Using code like if(callback) can make your application more flexible, and not break as easily if you don’t always need a response.
  • Build in error responses. Use if(), switch(), and try() to pass different parameters to callback() instead of the same value no matter what (or having a blank response when it could be more informative).

See Also

  • jQuery’s $.Deferred() method, for developing your own asynchronous functions with detailed responses.
Drew Powers is a frontend developer at Envy.