You Can throw() Anything In JavaScript

0
100


For the last few months, I’ve been listening to Ryan Toronto and Sam Selikoff talk about React Suspense over on the Frontend First podcast. I don’t know much of anything about React Suspense, but it appears to work, at least in part, by throw()ing Promise objects in JavaScript. Obviously, the overwhelming majority of throw() statements within a client-side application will use Error instances. But, the fact that Suspense is throwing Promises got me wondering: can you throw() anything in JavaScript?

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

To explore this, all I did was create an Array of values, loop over those values, and try to throw() them:

<!doctype html>
<html lang="en">
<head>
	<meta charset="utf-8" />
	<meta name="viewport" content="width=device-width, initial-scale=1" />
	<title>
		throw() Anything In JavaScript
	</title>
</head>
<body>

	<h1>
		throw() Anything In JavaScript
	</h1>

	<script type="text/javascript">

		// Let's create a collection of different types of JavaScript objects to see what
		// happens when we throw() them around.
		var values = [
			null,
			undefined,
			true,
			false,
			1234,
			new Date(),
			"String Object",
			[ "Array Object" ],
			{ type: "Object Object" },
			new Map().set( "foo", "bar" ),
			new Set().add( "foo" ),
			Promise.resolve( "Promise Object" ),
			Promise.reject( "Rejection Object" ),
			new Error( "Error Object" )
		];

		console.group( "Trying to throw() various objects in JavaScript." );

		for ( var value of values ) {

			try {

				throw( value );

			} catch ( error ) {

				console.warn( "%cCatch:", "font-weight: bold", error );

			}

		}

		console.groupEnd();

	</script>

</body>
</html>

Ultimately, most things in JavaScript “extend” (ie, have in their prototype chain) the Object constructor. As such, most of these tests are redundant. That said, I tried to create all the objects I could think of on the fly. And, when we run this JavaScript code, we get the following output:

Console logging demonstrating that each value that was thrown was then caught in the catch block and logged.

As you can see, each value in my collection was successfully used in the throw() statement and then consumed in the catch block. So, not only can you throw Promise objects in JavaScript, you can throw … anything!

throw() anything other than an Error instance. However, if we shift our mindset over to Promises for a moment – and think about Promise rejections, not errors – then the lines get a little more fuzzy. While I might never think to throw() anything other than an Error, I would certainly consider using non-Error objects in my Promise rejections.

In fact, when I was building my fetch()-powered API client in JavaScript, part of the guarantee that it makes is that it will catch all internal errors and normalize them such that there is a consistent structure for all reasons that result in a Promise rejection. An abbreviated version of this code looks like:

async makeRequest() {

	try {

		var fetchResponse = await fetch( ... );
		var data = await this.unwrapResponseData( fetchResponse );

		if ( ! fetchResponse.ok ) {

			return( Promise.reject( this.normalizeError( data ) ) );

		}

	} catch ( error ) {

		return( Promise.reject( this.normalizeTransportError( error ) ) );

	}

}

As you can see here, the rejections in this API client are all passing through a “normalization” process that returns an Object that is used as the rejection of the API call. And, for me, this feels completely natural and correct.

Now, to bring this back to the contemplation of throw(): in this case, I’m returning an explicit rejection. However, one of the wonderful things about async/await Functions is that they will automatically catch errors and parle them into Promise rejections. Which means, I can theoretically take the above code and re-write it using throw() instead of Promise.reject():

async makeRequest() {

	try {

		var fetchResponse = await fetch( ... );
		var data = await this.unwrapResponseData( fetchResponse );

		if ( ! fetchResponse.ok ) {

			throw( this.normalizeError( data ) );

		}

	} catch ( error ) {

		throw( this.normalizeTransportError( error ) );

	}

}

NOTE: I have not run this code – I’m just riffing off my mental model. So, forgive me if there are syntax errors or mistakes here.

These two blocks of code lead to the same exact behavior: they return a Promise that is (in the case of an error) rejected with the normalized error Object. And yet, the Promise.reject() syntax feels so natural while the throw() syntax feels so freaking strange.

It makes me question: does one of these syntax approaches express clearer intent?

And, if I’m being honest, the more I stare at this, the more the throw() approach feels like it explains the workflow better. Or, at least, more consistently. If async/await is syntactic sugar over the use of Promises, it feels a bit odd that I’m pulling in Promise.reject() as part of the control-flow – it feels like I’m mixing two different paradigms (even through they are technically the same exact thing).

On the other hand, throw() feels like it’s living at the correct syntactic sugar level. If an async function will naturally turn non-errors in Promise fulfillments and errors into Promise rejections, then using throw() feels like the most consistent way to “return” the errors.

Something magical happened here: I started writing this post thinking it would just be a fun exploration of the throw() statement. But, I ended up completely questioning my mental model. And, when all was said and done, I think I’ve actually started to evolve my thinking. Yesterday, the idea of throwing an “Object” in JavaScript felt gross. Today, I think it kind of makes sense.





Source link

Leave a reply

Please enter your comment!
Please enter your name here