Skip to main content
Ben Nadel at Scotch On The Rock (SOTR) 2010 (Munich) with: Michael Hnat
Ben Nadel at Scotch On The Rock (SOTR) 2010 (Munich) with: Michael Hnat@madmike_de )

You Can throw() Anything In JavaScript - And Other async/await Considerations

By on

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!

Considering Promise Rejections And async/await

Honestly, it would never occur to me to 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 ) ) );

		}

		return( 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.

UPDATE ON 2022-02-08: I've modified the following code after logical errors were pointed out in the comments below. I originally had a single try/catch around the internals of the function, which was causing one error to be swallowed inappropriately. To remedy this, I've broken the code up into two separate try/catch blocks that each have different responsibilities.

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 );

	} catch ( error ) {

		throw( this.normalizeTransportError( error ) );

	}

	if ( ! fetchResponse.ok ) {

		throw( this.normalizeError( data ) );

	}

	return( data );

}

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.

Want to use code from this post? Check out the license.

Reader Comments

16 Comments

I wonder if part of the reason that throw() felt awkward or unnatural to you is because it's not actually a function-call, and therefore does not generally need the the (..) part surrounding what you throw? It's just a keyword/operator.

You correctly name it a "throw statement" (it's not an expression!), but then everywhere you list it, you style it as throw(). I personally think it looks weird as a function call, because it has a magical side-effect that stops all flow control. But once you take the parens off (throw new Error(..)), to me it looks more natural like an intentional side-effect that should interrupt the flow control.

15,260 Comments

@Kyle,

My obsession with parenthesis dates waaaaaay back to my QBasic days. I remember when my teacher told me that, in QBasic, a Function invocation could only have parenthesis if it returned a value; and that a Function that didn't return a value was actually just a "Subroutine" and couldn't be invoked with parenthesis.

And, I was like, WAT?!?! ๐Ÿ˜ฑ I want parenthesis everywhere!!

You'll also see me use them with the typeof operator, as in:

if ( typeof( foo ) === "string" ) { ... }

And, if I have a lot of conditions together, I never never never reply on operator precedence - I'm wrapping nested parenthesis around all those suckers.

if ( ( ( a == 3 ) && ( b == 4 ) ) || ( c == 9 ) ) { ... }

I know that these are not apples-to-apples comparison usage of parenthesis - I'm just trying to underscore that I'm definitely on Team Parenthesis even when I don't technically need them. I like to think of them like hugging the values ๐Ÿ˜Š

Ok, sorry for the side-quest there on parenthesis. I'm just a fan.

I understand that there is a difference between a statement and an expression; but, I don't believe that I could articulate it. I'm actually mid-way through Dr. Axel Rauschmayer's book on ES2022 and he has a whole section on expressions vs. statements ... and my brain just refuses to keep it in my head.

As far as throwing things other than Errors, I think I just never learned that. I remember, in the early days, how much everyone stressed throwing new Error() in order to get the stack-traces populated. It never occurred to me that it made sense with anything else. But, I'm kind of excited to have that mental model fleshed-out. I think it's going to make my async/await code much more intentful.

2 Comments

Hi Ben,
in your transformed example, the inner error (normalizeError) will always be catched and turned into an outer error (normalizeTransportError).
This is a huge difference to your original example where you return rejected promises. ;-)
Cheers,
Tobias

15,260 Comments

@Tobias,

I am not sure that I understand what you mean? What I am saying is that from the caller's stand-point - ie, the code that is invoking the async function, the async function will always return a Rejected promise if there is an error internally, regardless of whether or not that is triggered as a throw() or as a Project.reject(). Are you saying that I am incorrect here in my analysis?

2 Comments

@Ben,

No, you are totally right with your analysis. I'm just saying that your two examples are not equivalent.

In your async function makeRequest you have a huge try-catch block. The line

throw( this.normalizeError( data ) );

is inside the "try" block. Hence, it will get catched and the whole function rejects with this.normalizeTransportError( error ) instead of this.normalizeError( data ) as intended.

15,260 Comments

@Tobias,

Oooooooooh, I see what you are saying now! ๐Ÿ˜ณ Yes, that's a great catch! You are completely right! That is an oversight in my thinking. I never actually ran that code - I was just thinking about it. Oh man, see - this is what happens when you don't actually run code.

For anyone else that was initially unclear on this, what Tobias is saying is that my first throw:

throw( this.normalizeError( data ) );

... is inside a try/catch block. Which means, it will get caught by the catch, which in this case, turns around and throws a different error:

throw( this.normalizeTransportError( error ) );

So, essentially, my first error is always being swallowed / wrapped.

Probably, what I would need to do is move the network-related stuff into its own try/catch block. It's hard to think about because this was just pseudo-code; but, maybe something like:

async makeRequest() {

	try {

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

	} catch ( error ) {

		throw( this.normalizeTransportError( error ) );

	}

	if ( ! fetchResponse.ok ) {

		throw( this.normalizeError( data ) );

	}

	return( data );

}

Again, great catch ๐Ÿ™Œ

36 Comments

Surprised to see your name on the front page of Hacker News today ๐Ÿ˜†

Not sure I'd feel entirely comfortable with something that hasn't at least extended an Error being thrown. Simply for the fear of having error handling code somewhere that would only expect something of that format suddenly having to deal with another type.

It'd be try catches all the way down ๐Ÿ˜…

1 Comments

NOTE: I have not run this code

I never understood why people go to the effort of writing some code, and then writing a comment to say they didn't run it. It's not like it's a lot of effort to run.

And because you didn't bother you look like a plum, since it turns out that the code you wrote made a glaring error. One that your article seems partly predicated upon. All completely avoidable.

Just run the code, man.

15,260 Comments

@David,

Good sir, excellent to hear from you! I'll have to check out Hacker News :D But yeah, it feels strange to throw things that aren't in some way an extension of the Error class. I'm still trying to get it all settled in my head.

15,260 Comments

@Bongo,

You are correct - it was just laziness on my part. And it's unfortunate that it was such a misstep. I really should go back and update the code as I think it will make things more clear.

That said, the premise of the article is sound. And the main example, which is the series of throw() operations on different data types was executed. But then I muddy the water with a poor follow-up example on Promises ๐Ÿ˜ž

15,260 Comments

@Tobias, @Bongo,

I've updated the code example to have the more correct try/catch blocks. I still haven't run the code because I don't have it handy at this time. But, at least this should remove the confusion. Thanks for keeping me honest ๐Ÿ’ช

Post A Comment — I'd Love To Hear From You!

Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.