Checked Exceptions: The Language Design Debate Java Never Resolved
Why Java stands almost alone among mainstream languages in forcing you to declare or catch certain exceptions, the criticism that shaped C# and Kotlin, and whether the reasoning behind it held up.
Every Java developer has typed throws IOException without thinking twice about it, and most have also written catch (Exception e) {} just to make a stubborn compiler error go away. Both habits trace back to a single design decision made in the mid-1990s: Java would enforce, at compile time, that certain exceptions must be either caught or declared. Almost no other mainstream language followed that path, and the ones that came after Java went out of their way to explain why they weren’t going to.
What Checked Exceptions Were Designed to Do
Java 1.0 split the exception world into three buckets. Checked exceptions, things like IOException or SQLException, represent conditions a well-written program should anticipate and recover from, and the compiler forces every method up the call chain to either handle one or admit in its own signature that it might occur. Unchecked exceptions, subclasses of RuntimeException, represent programming errors like a null dereference or an out-of-bounds index, and the compiler stays silent about them. Error covers conditions like running out of memory that no reasonable method should be expected to catch.
The intent was reasonable on its face. A method’s throws clause becomes part of its documented contract, checked automatically by the compiler rather than left to comments that go stale. Callers cannot forget to consider a failure mode the API author already knew was likely. For an I/O-heavy standard library shipping in a language aimed at reliability, this looked like a natural extension of static typing into the world of error handling.
The Hejlsberg Critique That Shaped C#
By the time C# was taking shape at Microsoft in the early 2000s, checked exceptions had accumulated real scar tissue among working programmers. In an August 2003 interview conducted by Bill Venners and Bruce Eckel, C#’s lead architect Anders Hejlsberg laid out why the language would leave them out entirely. He described picking up APIs full of throws clauses and watching the resulting code become tangled and defensive rather than safer.
Hejlsberg’s argument centered on two concrete failure modes rather than a general dislike of the concept. The first was versionability: add a new checked exception to a method in a library’s next release, and every caller that compiled cleanly against the old signature now fails to build, even if that particular caller never runs the code path that triggers the new exception. The second was scalability: checked exceptions do not compose cleanly as call chains get deeper, since each layer either has to catch and handle a failure it usually cannot meaningfully act on, or repeat the exception in its own signature and pass the obligation further up. In small examples this looks disciplined. Across a large codebase with many layers of abstraction, it tends to produce methods whose signatures are dominated by exception plumbing rather than by what the method actually does.
C# shipped in 2002 without checked exceptions, and Hejlsberg’s interview, published the following year, gave the industry a clear articulation of why Java’s approach was considered a mistake by an architect who had just built the highest-profile competing language.
The Rest of the Field Agreed, Explicitly
What makes checked exceptions unusual is not just that Java has them, it is how consistently every major language designed afterward chose not to, often on the record. Kotlin’s own documentation makes the case with a real JDK example: the Appendable interface declares append(CharSequence) as throwing IOException, which means every implementation of Appendable, including ones that never touch a file or a socket, is forced to either handle or propagate an I/O exception that can never actually occur in their context. Kotlin’s designers took that as evidence the mechanism was adding ceremony without adding safety, and Kotlin has never had checked exceptions since its 1.0 release.

| Language | First stable release | Checked exceptions |
|---|---|---|
| Java | 1996 | Yes, enforced by the compiler |
| Python | 1991 | No |
| JavaScript | 1995 | No |
| C# | 2002 | No, deliberately excluded |
| Scala | 2004 | No |
| Go | 2012 | No exceptions at all; errors are ordinary return values |
| Swift | 2014 | Partial: throws forces acknowledgment but not per-type checking |
| Rust | 2015 | No exceptions at all; uses the Result type |
| Kotlin | 2016 | No, explicitly rejected in official documentation |
Even C++, which predates Java, tried a related idea and abandoned it. Dynamic exception specifications let a function declare throw(SomeType) to list what it might throw, but the mechanism was never checked as strictly as Java’s and quickly earned a reputation for being unenforceable in practice. The C++ standards committee deprecated it in the C++11 standard and removed it outright in C++17, keeping only the simpler noexcept marker that says whether a function throws at all, with no attempt to enumerate specific types.
Did the Original Justification Hold Up?
The strongest test of a language feature is not what its designers intended, it is what programmers actually do with it once it ships at scale. A 2016 empirical study presented at the International Conference on Mining Software Repositories, by Nakshatri, Hegde, and Thandra, mined real Java projects to see how checked exceptions were being handled in practice, and the results were not flattering.

Two findings stand out. Catch blocks reached for the generic Exception or Throwable type instead of a specific subclass the large majority of the time, which defeats much of the precision checked exceptions were meant to provide in the first place. And a full fifth of catch blocks did nothing at all, an empty handler that satisfies the compiler while quietly discarding the failure. The study also found that logging the error without attempting recovery was the single most common action taken, and that converting a checked exception into an unchecked one purely to make it stop being the caller’s problem was a routine workaround rather than a rare escape hatch.
None of this proves checked exceptions cause bad error handling on their own. Careless catch blocks exist in every language. But it does suggest the compiler enforcement that was supposed to guarantee thoughtful handling mostly succeeded at guaranteeing that something, anything, appears inside the catch block, which is a considerably weaker outcome than the feature was designed to produce.
Where checked exceptions still earn their keep:
- A small, stable set of well-understood recoverable failures, such as a file not being found
- APIs with a narrow surface area where the throws clause stays short and specific
- Domains where regulatory or safety requirements make explicit, auditable error handling valuable even at the cost of verbosity
Where they tend to backfire:
- Deep call chains and generic or functional-style code, where every layer must repeat or launder the exception type
- Interfaces implemented by many unrelated classes, where one implementation’s failure mode leaks into every other implementation’s signature
- Library evolution, where adding a single new checked exception in a minor release can break every downstream caller’s build
What We Have Learned
Checked exceptions were a genuine attempt to make error handling a first-class, compiler-verified part of an API’s contract, and the idea was taken seriously enough that C++ tried a lighter version of it before quietly removing it. What the last two decades show is that the enforcement mechanism did not reliably produce the careful handling it was designed to guarantee. Anders Hejlsberg’s versionability and scalability objections predicted, in 2003, exactly the kind of code the 2016 empirical study later found in the wild: generic catches, empty handlers, and checked exceptions laundered into unchecked ones just to get the compiler to stop complaining. Java has never removed the feature, and changing it now would break a language used by millions of existing programs, so it remains a live design choice future languages keep explicitly reacting against rather than a settled question Java itself ever closed.

