Challenge #10: Exceptional Responses

What happens if an endpoint is configured with an array of responses and the array runs out? In this challenge, you’ll make the code fail fast with an informative error.

Instructions

1. In the "throws exception when list of configured responses runs out" test:

  1. Configure the Nulled HttpClient with an endpoint that has one response.
  2. Assert that http.requestAsync() throws a helpful error the second time it’s called on that endpoint.

2. In the production code:

  1. Modify the embedded stub to make the test pass.

Remember to commit your changes when you’re done.

API Documentation

await assert.doesNotThrowAsync(fnAsync);

Assert that an asynchronous function does not throw an error. Use it like this:

await assert.doesNotThrowAsync(
	() => transformAsync(...),    // note: NO await here
);
  • fnAsync (function) - the function to run
await assert.throwsAsync(fnAsync, expectedError);

Assert that an asynchronous function throws an error. Use it like this:

await assert.throwsAsync(
	() => transformAsync(...),    // note: NO await here
	"my expected error",
);
  • fnAsync (function) - the function to run
  • expectedError (string | regex) - the error fnAsync is expected to throw
const length = array.length;

Get the number of entries in an array.

  • returns length (number) - the number of entries

JavaScript Primers

No new concepts.

Hints

1
The test needs to configure an endpoint with a single response.
Remember that endpoints configured without an array have an infinite number of responses.
To configure a single response, you have to put it in an array.
it("throws exception when list of configured responses runs out", async () => {
	const client = HttpClient.createNull({
		"/endpoint": [{ status: 200, body: "my body" }],
	});
});
2
Assert that the first request doesn’t throw an exception.
You can use assert.doesNotThrowAsync() for that. Don’t forget to await it.
it("throws exception when list of configured responses runs out", async () => {
	const client = HttpClient.createNull({
		"/endpoint": [{ status: 200, body: "my body" }],
	});

	await assert.doesNotThrowAsync(
		() => requestAsync({ client, path: "/endpoint" }),
	);
});
3
Assert that the second request does throw an exception.
You can use assert.throwsAsync() for that. Don’t forget to await it.
it("throws exception when list of configured responses runs out", async () => {
	const client = HttpClient.createNull({
		"/endpoint": [{ status: 200, body: "my body" }],
	});

	await assert.doesNotThrowAsync(
		() => requestAsync({ client, path: "/endpoint" }),
	);

	await assert.throwsAsync(
		() => requestAsync({ client, path: "/endpoint" }),
		"No more responses configured in Nulled HTTP client",
	);
});
4
The test is ready to run.
It should fail with "expected exception".

That’s because your code isn’t throwing an exception when it runs out of items.

5
Modify your production code to throw an exception when the array runs out.
You can tell how many entries are left in the array with array.length.
The nextResponse() function is a good place to put it.
function nextResponse(responses) {
	if (Array.isArray(responses)) {
		if (responses.length === 0) throw new Error("No more responses configured in Nulled HTTP client");
		return responses.shift();
	}
	else {
		return responses;
	}
}
6
When you run the test, it fails! Why? Isn’t the assertion supposed to catch the exception?
This one is tricky. Take a close look at the stack trace.
The test isn’t in the stack trace.
The exception is being thrown inside setImmediate().
setImmediate() schedules code to be run in the future.

Because the exception is being thrown inside setImmediate(), it has its own stack frame, which means that assert.throwsAsync() doesn’t get the exception. To prevent the problem, the exception needs to be thrown outside of setImmediate().

7
Make the test pass.
Make sure the exception isn’t thrown inside setImmediate().
One way to do so is to move the call to nextResponse().
class StubbedRequest extends EventEmitter {

	constructor(responses) {
		super();
		this._responses = responses;
	}

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

}

Complete Solution

Test code:
it("throws exception when list of configured responses runs out", async () => {
	const client = HttpClient.createNull({
		"/endpoint": [{ status: 200, body: "my body" }],
	});

	await assert.doesNotThrowAsync(
		() => requestAsync({ client, path: "/endpoint" }),
	);

	await assert.throwsAsync(
		() => requestAsync({ client, path: "/endpoint" }),
		"No more responses configured in Nulled HTTP client",
	);
});
Production code (JavaScript):
class StubbedRequest extends EventEmitter {

	constructor(responses) {
		super();
		this._responses = responses;
	}

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

}

function nextResponse(responses) {
	if (Array.isArray(responses)) {
		if (responses.length === 0) throw new Error("No more responses configured in Nulled HTTP client");
		return responses.shift();
	}
	else {
		return responses;
	}
}
Production code (TypeScript):
class StubbedRequest extends EventEmitter {

	constructor(private readonly _responses?: NulledHttpClientResponse | NulledHttpClientResponse[]) {
		super();
	}

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

}

function nextResponse(responses?: NulledHttpClientResponse | NulledHttpClientResponse[]) {
	if (Array.isArray(responses)) {
		if (responses.length === 0) throw new Error("No more responses configured in Nulled HTTP client");
		return responses.shift();
	}
	else {
		return responses;
	}
}

Next challenge

Return to module overview