JavaScript Concurrency

This primer explains how concurrency works in JavaScript.

See all primers

Contents

Concurrency Fundamentals

JavaScript is single-threaded, but it’s still capable of running operations in the background. The background operations are handled by the runtime environment—Node.js, in this case.

Most background operations in this course relate to sending HTTP requests and waiting for responses. Node.js has a library for making those requests. When we call that library, Node starts a background operation, then sends us events as the background operation progresses. We use promises to control how we respond to those events.

Top

await Quick Reference

In this codebase, some functions (and methods) have a name ending in Async(). That means they return a Promise object rather than a normal return value. They usually execute some of their code asynchronously, in the background. They’re not done until the promise “resolves.”

Whenever you see a function with a name ending in Async(), use the await keyword to cause your code to wait until the promise resolves.

const result = await someFunctionAsync();

That’s all you need to know for the majority of the exercises in this course. For more information about concurrency in JavaScript, start at the top of this primer and read the whole thing.

Top

async Quick Reference

If your function uses the await keyword, it has to be marked async. In most cases, I’ve already done this for you. However, if you factor out a helper function, you might need to mark it async. Some arrow functions might need to be marked async, too.

async function myFunctionAsync() {...}  // function
async myMethodAsync() {...}             // method
async () => {...}                       // arrow function

The async keyword causes JavaScript to wrap your function’s return value in a Promise. That means any code calling your function will need to await the return value. For consistency with the rest of the code, give your async functions a name ending in Async().

Top

Using Promises

A function that is waiting for a background operation will return a Promise object. (In this codebase, they all have names ending in Async.) A promise represents the eventual completion, or failure, of that background operation. The promise is returned immediately, before the background operation completes.

async function runBackgroundTaskAsync() {
  // do something in the background
  console.log("After task completes");

	return "foo";
}

console.log("Before running task");
const myPromise = runBackgroundTaskAsync();
console.log("After running task");
console.log("Promise: " + myPromise);

// -- output --
// Before running task
// After running task
// Promise: Promise { <pending> }
// After task completes

To wait for the background operation to complete, and get its return value, use the await keyword. This causes JavaScript to stop executing the current code. When the background operation completes, the promise will resolve, and the rest of your code will continue.

async function runBackgroundTaskAsync() {
  // code that does something in the background
  console.log("After task completes");

	return "foo";
}

console.log("Before running task");
const result = await runBackgroundTaskAsync();
console.log("After running task");
console.log("Result: " + result);

// -- output --
// Before running task
// After task completes
// After running task
// Result: foo

If the background operation fails, the promise will reject with an exception, and await will throw that exception. You can handle the exception with a normal try/catch block.

async function runBackgroundTaskAsync() {
  // code that does something in the background and fails
  console.log("After task completes");

	return "foo";
}

try {
	console.log("Before running task");
	const result = await runBackgroundTaskAsync();
	console.log("After running task");
}
catch (err) {
	console.log("Background task failed");
}

// -- output --
// Before running task
// Background task failed

Rarely, you’ll want to start the background operation, do something else, and then wait for the operation to complete. You can do that by saving the promise in a variable and awaiting it later.

async function runBackgroundTaskAsync() {
  // code that does something in the background
  console.log("After task completes");

	return "foo";
}

console.log("Before running task");
const myPromise = runBackgroundTaskAsync();
console.log("After running task");
console.log("Promise: " + myPromise);

console.log("Result: " + await myPromise);
console.log("Resolved promise: " + myPromise);

// -- output --
// Before running task
// After running task
// Promise: Promise { <pending> }
// After task completes
// Result: foo
// Resolved promise: Promise { "foo" }

Top

Creating Promises

Although promises are the main way modern JavaScript programs handle background operations, most Node.js libraries don’t use promises. Instead, they use callbacks. Your code needs to turn those callbacks into promises.

A callback is an arrow function that Node.js calls when a background operation is complete. The classic example is setTimeout(), which waits n milliseconds, then calls the callback.

console.log("Before calling setTimeout()");
setTimeout(() => {
  console.log("Inside setTimeout() callback");
}, 10000);
console.log("After calling setTimeout()");

// -- output --
// Before calling setTimeout()
// After calling setTimeout()
// ...10 seconds later...
// Inside setTimeout() callback

To convert a callback into a promise, create an instance of the Promise class. It has the following signature:

const promise = new Promise((resolve, reject) => {...});

The Promise constructor takes an arrow function with resolve and reject parameters. Those parameters are functions. When the background function completes, call resolve(returnValue). If it fails, call reject(errorValue).

Here’s how it works with setTimeout():

console.log("Before creating promise");
const timeoutPromise = new Promise((resolve, reject) => {

	console.log("Before calling setTimeout()");
	setTimeout(() => {
	  console.log("Inside setTimeout() callback");
		return resolve("my_result");
	}, 10000);
	console.log("After calling setTimeout()");

});
console.log("After creating promise");

console.log("About to await promise");
const result = await timeoutPromise;
console.log("After awaiting promise: " + result);

// -- output --
// Before creating promise
// Before calling setTimeout()
// After calling setTimeout()
// After creating promise
// About to await promise
// ...10 seconds later...
// Inside setTimeout() callback
// After awaiting promise: my_result

And here’s a side-by-side comparison without the logging:

// Without promises:
setTimeout(() => {
	// after 10 seconds, execution continues here
}, 10000);

// With promises:
await new Promise((resolve, reject) => {
	setTimeout(() => {
		resolve();
	}, 10000);
});
// after 10 seconds, execution continues here

// With promises (compact version):
await new Promise((resolve, reject) => {
	setTimeout(() => resolve(), 10000);
});
// after 10 seconds, execution continues here

Top

Using Events

Most Node.js background operations communicate with your JavaScript code using events. When something happens in the background, Node calls a callback that you registered with an EventEmitter. This is the signature you use to register the callback:

emitter.on(eventName, () => {...}));

Here’s an example of how you would use events to make an HTTP request:

const request = http.request(/* options */);
request.end();

request.on("response", (response) => {
	response.resume();  // Ignore the response body
	response.end("end", () => {
		console.log("Status: " + response.statusCode);
		// execution continues here
	});
});

Let’s break that down:

console.log("Making request");

// Start the request. The 'request' return value is an EventEmitter.
const request = http.request(/* options */);
request.end();

// Register for the "response" event. This event occurs when the beginning of the response is received.
request.on("response", (response) => {
	console.log("Receiving response");

	response.resume(); // Ignore the response body

	// The 'response' parameter is also an EventEmitter.
	// Register for the "end" event. This event occurs when the response is complete.
	response.end("end", () => {
		console.log("Response complete");
		console.log("Status: " + response.statusCode);
		// execution continues here
	});
	console.log("Registered for 'end' event");
});
console.log("Registered for 'response' event");

// -- output --
// Making request
// Registered for 'response' event
// Receiving response
// Registered for 'end' event
// Response complete
// Status: 200

To make the control flow easier to work with, you’ll typically create a promise for the background operation. For example, the following code creates a promise that resolves when the request is complete:

console.log("Before the promise");
const status = await new Promise((resolve, reject) => {

	// Start the request. The 'request' return value is an EventEmitter.
	console.log("Making request");
	const request = http.request(/* options */);
	request.end();

	// Register for the "response" event. This event occurs when the beginning of the response is received.
	request.on("response", (response) => {
		console.log("Receiving response");

		// Ignore the response body
		response.resume();

		// The 'response' parameter is also an EventEmitter.
		// Register for the "end" event. This event occurs when the response is complete.
		response.end("end", () => {
			console.log("Response complete");
			resolve(response.statusCode);
		});
		console.log("Registered for 'end' event");
	});
	console.log("Registered for 'response' event");

});
console.log("After the promise");
console.log("Status: " + status);
// execution continues here

// -- output --
// Before the promise
// Making request
// Registered for 'response' event
// Receiving response
// Registered for 'end' event
// Response complete
// After the promise
// Status: 200

Here’s what the code looks like without all the comments and logging:

const status = await new Promise((resolve, reject) => {

	const request = http.request(/* options */);
	request.end();

	request.on("response", (response) => {
		response.resume();
		response.end("end", () => {
			resolve(response.statusCode);
		});
	});

});
console.log("Status: " + status);
// execution continues here

Top

Creating Event Emitters

To make a class into an event emitter, extend the EventEmitter class. This will cause your code to inherit the on() method, which users of your class will use to listen for events.

If your class has a constructor, you have to add a call to super(); on the first line. That runs the superclass’s constructor.

To emit an event, call the emit() method, which is also inherited from EventEmitter. It has this signature:

this.emit(eventName, eventValue);

For example, if you wanted to create a class that emitted a timeout event after a certain amount of time, you would write this code:

class Timer extends EventEmitter {

	start(timeoutInMilliseconds) {
		setTimeout(() => {
			this.emit("timeout", timeoutInMilliseconds);
		}, timeoutInMilliseconds);
	}

}

Then you could use it like any other event:

const timer = new Timer();

console.log("Starting timer");
timer.start(10000);

console.log("Before listening for event");
timer.on("timeout", (timeout) => {
	console.log("Timer elapsed: " + timeout);
});
console.log("After listening for event");

// -- output --
// Starting timer
// Before listening for event
// After listening for event
// ...10 seconds later...
// Timer elapsed: 10000

In this course, you’ll create stubs of Node classes. That involves implementing methods that do nothing but emit specific events. For example, a stub of Node’s http.ClientRequest class emits the "response" event when the end() method is called:

// Incorrect. Do not copy.
class StubbedRequest extends EventEmitter {

	end() {
		this.emit("response", new StubbedResponse());
	}

}

But that code doesn’t fully match the real http.ClientRequest behavior. Real events are asynchronous, and the call to this.emit() in the above example is synchronous. This leads to subtle mismatches in behavior.

// Real behavior
const request = http.request(/* options */);
console.log("Calling request.end()");
request.end();

request.on("response", (response) => {
	console.log("Receiving response");
	/* ... */
});
console.log("Registered for 'response' event");

// -- output --
// Calling request.end()
// Registered for 'response' event
// (the "response" event is emitted asynchronously)
// Receiving response


// Stub's behavior
const request = new StubbedRequest();
console.log("Calling request.end()");
request.end();

request.on("response", (response) => {
	console.log("Receiving response");
	/* ... */
});
console.log("Registered for 'response' event");

// -- output --
// Calling request.end()
// (the "response" event is emitted synchronously)
// Registered for 'response' event

In the stubbed version, the "Receiving response" log is never written, because the "response" event occurs before the event handler is registered.

To make your stubs work properly, they need to emit their events asynchronously. You can do that with setImmediate(). It has this signature:

setImmediate(() => {...});

Use it like this:

// Correct version.
class StubbedRequest extends EventEmitter {

	end() {
		setImmediate(() => {
			this.emit("response", new StubbedResponse());
		});
	}

}

Now the stub matches the real-world behavior.

const request = new StubbedRequest();
console.log("Calling request.end()");
request.end();

request.on("response", (response) => {
	console.log("Receiving response");
	/* ... */
});
console.log("Registered for 'response' event");

// -- output --
// Calling request.end()
// Registered for 'response' event
// (the "response" event is emitted asynchronously)
// Receiving response

Top