Skip to main content
Ben Nadel at CF Summit West 2024 (Las Vegas) with: Sam Effa
Ben Nadel at CF Summit West 2024 (Las Vegas) with: Sam Effa

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

By
Published in Comments (14)

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,902 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,902 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,902 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,902 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,902 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,902 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 💪

1 Comments

@David,

Interestingly enough typescript types caught errors as unknown and only lets you type them as any or unknown manually so I think that it is a best practice to assume that errors could be of any type not just Error

1 Comments

I found your post after googling "throw promise" as I was curious if the try/catch mechanism could or should be abused to write a simple API that most of the time returns values synchronously, unless the value can't be evaluated without some internal Async code being executed ( for example a server fetch or indexed db get/ decompression/decryption etc)
If a value definitely is not available, it could throw an error, and if it's possibly available it could throw a promise, which either resolves or rejects - kicks the can down the road so to speak

This would mean you could write code that most of the time behaves synchronously, until it can't. The caller csn then decide if it wants to wait or just give up, if, for example the caught promise means a ui just gets updated after the fact.

15,902 Comments

@Jonathan,

So, I think that is basically what React.js is doing with "suspense boundaries". To be clear, I am not a React developer, so take this with a grain of salt; but, from what I think I've heard on podcasts, you can throw() a Promise and React will either use it immediately if the data is available, or block-and-wait for the data to become available.

I'm not sure that exactly speaks to your point; but, is evidence that people "in the wild" are throwing objects as way to figure out if the rest of the request can proceed as planned.

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

Post a Comment

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel