Checked Exceptions

There is a language feature called “checked exceptions” that I constantly find myself wanting in C++. Basically, you would add the exceptions the function throws to the function signature. Something like:

1
2
3
4
5
6
7
8
9
10
11
12
//Basic implementation of normalizing a vector
void normalize( float vec[3] ) throws(ErrorZeroLength)
{
	float len_sq = dot( vec, vec );             //Length of vector squared
	float recip_len = 1.0f / std::sqrt(len_sq); //Reciprocal of length

	//Fail if we were passed a zero vector
	if (std::isnan(recip_len)) [[unlikely]] throw ErrorZeroLength();

	//Divide vector by length -> normalizes it
	for ( size_t i=0; i<3; ++i ) vec[i]*=recip_len;
}

The idea is that any caller of normalize(⋯) is immediately aware that the callee can throw an exception of a particular type (in this case, an instance of ErrorZeroLength)—in fact, the callee is required to handle this.

The idea of a checked exception is that the caller is forced, by the compiler, to handle this exception (or, if it does not handle it, to declare that it throws it upward again, to its caller). Thus, there is very careful accounting of the exceptions that are “designed” to be in-play. Note that checked exceptions (whose type should be checked by the compiler at compile time) are not the same as unchecked exceptions (which can be of any type, and whose types are not enforced by anything, including the compiler).

In languages like Java that actually already have this feature, the actual checking of the checked exception happens at runtime, which is obviously asinine. It could instead easily be checked at compile-time in the same way that the type signature is checked: when compiling the function definition, the compiler forces you to add the exception specification to the declaration. Now that you’ve updated the declaration, all functions can see it, and when they’re compiled, you update them recursively upward, too.


Checked exceptions actually were added into C++ . . . but they weren’t actually checked; they were just fluff added to look good; the compiler didn’t enforce anything! The (throws(...) specifications) feature was therefore removed in C++17, because it was pointless. It’s 2021, so I’m using C++20, as is fitting and proper, which means that the only thing that survives is the noexcept specifier (i.e. what was formerly given by throws(), meaning no exceptions)—which is still unchecked, but at least lets the compiler do some runtime optimizations.


Today, people are very hostile to the idea of checked exceptions in many languages, including and especially C++. The main counterargument is that changing a lower-level function could cause ripple-out changes throughout the codebase to exception specifications. Yet this is . . . exactly the point! I absolutely want my compiler to tell me, at compile time, every single place I might need to update my error-handling code. People who don’t handle errors are evil, and checked exceptions are the only practical way the rest of us can keep ourselves internally consistent when writing large systems.

One counterargument I heard that’s sortof okay is that the compile-time checks might not be possible. E.g. (adapting from an example on StackOverflow):

1
2
3
4
5
6
7
8
9
[[nodiscard]] float sqrt_realvals( float x ) throws(DomainError)
{
	if ( x < 0.0f ) throw DomainError();
	return std::sqrt(x);
}
[[nodiscard]] float sqrt_or_zero( float x ) noexcept
{
	return x>0.0f ? sqrt_realvals(x) : 0.0f;
}

Allegedly, the compiler cannot prove that sqrt_or_zero(...) doesn’t throw anything. Now, of course, it totally can in this particular case, but that doesn’t negate the overall argument. You could easily have a harder problem:

1
2
3
4
5
6
//Maybe cannot compile
[[nodiscard]] float sqrt_of_fnval( uint32_t x ) noexcept
{
	float arg = my_fn(x);
	return sqrt_realvals(arg);
}

. . . where you cannot reasonably expect the compiler to figure out if this is okay or not. In this case, my_fn(...) might have been constructed by the programmer to never put a 1 in the uppermost bit, but this could require arbitrarily complicated reasoning to prove—and its truthiness might even depend on how the program is linked. You simply can’t demand the inversion of a potentially non-invertible function.

That said, it’s not like there aren’t ways to fix this. For example, by adding asserts (which you should be doing anyway) you can make your intentions clear to the compiler (this is actually already supported with e.g. __assume(...), etc.).

So, while the above might not compile, this could:

1
2
3
4
5
6
7
//Now should compile
[[nodiscard]] float sqrt_of_fnval( uint32_t x ) noexcept
{
	float arg = my_fn(x);
	assert( arg >= 0.0f ); //Should have had this assertion *anyway*
	return sqrt_realvals(arg);
}

Or (using C++20 -> C++23 -> C++26 (?) contracts), you can add the constraint to the function:

1
2
3
4
5
6
7
[[nodiscard]] float my_fn( uint32_t x ) noexcept
	//[[ ensures result > 0.0f ]]
	post( result: result > 0 )
{
	float result = /*...*/;
	return result;
}

Return to the list of blog posts.