-
-
Notifications
You must be signed in to change notification settings - Fork 0
Research Exception Hierarchy
Question. Replace the generic JDK exceptions in net.onelitefeather.falco.anvil with a
dedicated hierarchy that has both checked and unchecked types, so that "the server never loads a
world in a strange state". This page is the design that came out of that question; it is not a
description of shipped behaviour, because none of it is implemented.
Method. Three agents worked on this — hierarchy semantics, a complete catalogue of every throw
site, and the world-consistency guarantees. The language rules below were established by compiling
probe code, which the investigation records as javac 25.0.3; the probes are not checked in. The
counts over the package were established by reading falco-anvil/src. They agreed on almost
everything and disagreed on one point that changes the whole migration.
Established. What Java permits and forbids for a mixed checked/unchecked hierarchy — four hard rules, each with the compiler error that produces it. A proposed set of six types. Seven rules the implementation would have to follow.
Open. Whether the checked root extends IOException. That decision is unmade and the two agents
reached opposite conclusions; both arguments are recorded below rather than resolved. Also open, and
newly so: whether moving AnvilChunkException into a new package is acceptable — see the correction
in What Java allows here.
True at. Written 2026-07-31, entered the repository at
fc0aef5. The counts and the two
corrections below were re-checked against falco-anvil/src at ca79507. Nothing here has been
implemented, so no statement on this page describes running code.
Two premises of this document were imported from another codebase and do not hold for Falco. They are corrected in place below, with the source citation that replaces them. One of them reverses the conclusion it was supporting. Everything else on the page survived the re-check.
Verified by compiling probe code, recorded by the investigation as javac 25.0.3.
A common root over checked and unchecked is impossible as a class. Java has exactly two roots,
Exception and RuntimeException, and a class cannot be both. The only bracket is an interface —
but it cannot be caught:
catch (AnvilFault f)
-> error: incompatible types: AnvilFault cannot be converted to Throwable
An interface therefore only helps after a broad catch, for instanceof or a pattern switch. It is
not a way to catch both families in one clause. This is worth stating plainly because the opposite
is a natural assumption.
Sealed exceptions compile, but give no exhaustiveness in catch. A try block that catches both
permitted subtypes still does not satisfy the compiler; a catch of the sealed supertype remains
necessary. Java has no exhaustiveness analysis for catch clauses. One reason to seal anyway survives:
it prevents a downstream project from breaking a pattern switch by adding a subtype.
Correction. The document originally offered a second reason — that the repository "already uses
sealed in eight places (MapEntry, ClickHolder, IItem, InventoryLayout, …)". Falco
declares no sealed type at all, verified at ca79507: grep -rn sealed over falco-anvil/src,
falco-light/src and falco-instance/src returns nothing, and none of the four named types exists
in this project. That premise was carried in from another codebase. Sealing the hierarchy would
therefore be the first use of sealed here, not a continuation of house style — which does not make
it wrong, but removes the argument that it is already settled practice.
All sealed members must live in one package. Falco has no module-info.java, so it runs in the
unnamed module, where a sealed class may only permit subtypes from its own package. Verified:
error: class Root in unnamed module cannot extend a sealed class in a different package
Consequence: either every exception type moves into …anvil.exception, or all stay in …anvil.
A mix breaks compilation.
Correction, and it reverses the conclusion. The document originally argued that moving
AnvilChunkException is safe "because it is @since 1.16.0 and the released version is 1.15.2, so
nothing depends on it yet". Both figures belong to another codebase. At ca79507,
AnvilChunkException
carries @since 0.1.0 and Falco's released version is 0.3.0, so the type has shipped in every
release the project has made and moving it is a source- and binary-breaking change for anyone who
catches it by name. What does license a move is a different fact on the same file: the class is
annotated @ApiStatus.Experimental, and its own Javadoc says the API "may still change while it is
being validated against real worlds". That is an argument the maintainer can accept or reject; it is
not the free move the original sentence described.
java.nio.file.Path must not be stored in an exception field. It is not serializable
(NotSerializableException: sun.nio.fs.UnixPath, verified), which would make the exception
unserializable. Store the region path as a String.
No serialVersionUID. The repository has zero occurrences across all five modules, re-checked at
ca79507; the build sets no -Xlint, and these exceptions never leave the process.
The two agents that looked at this reached opposite conclusions, and both arguments are sound.
For extending IOException — it is a migration question. The 40 throws clauses in
falco-anvil/src/main, both catch (IOException | RuntimeException) multi-catches in
FalcoAnvilLoader, and the 14 assertThrows(IOException.class) assertions in the tests keep working
untouched. The change becomes additive. All three counts were re-checked at ca79507 and are exact,
not approximate; they were also exact when the investigation ran at fc0aef5.
Against extending IOException — it is a correctness question. Every existing
catch (IOException) would silently keep catching the new types, including the swallowing block in
saveChunk. The migration would then be compile-time only and change nothing at runtime. The cost
(8 signatures in main) is exactly what the compiler is for.
This is a genuine trade-off between migration cost and enforcement, not a case where one side is wrong. It needs a decision before implementation.
The variant below is the one both agents converged on apart from the inheritance question. Six types
for the thirteen classes of the package that are not themselves an exception — the package holds 14
classes plus its package-info.java at ca79507, of which only AnvilChunkException is an
exception today. Both agents explicitly warned against more types, since one nobody catches is
Javadoc maintenance without a decision point.
| Type | Kind | Purpose |
|---|---|---|
AnvilFault |
sealed interface | Common contract, carries the ChunkLocation. Not catchable — for pattern switches after a broad catch. |
AnvilFormatException |
checked, abstract sealed | Root for everything the file itself got wrong. |
RegionFormatException |
checked, final | Broken .mca structure: header too short, implausible length field, overlapping sectors, unsupported compression scheme. |
ChunkDataException |
checked, final | Broken chunk NBT: missing key, wrong type, empty palette, index outside the palette. |
AnvilChunkException |
unchecked, non-sealed | The boundary type. Exists already; gains implements AnvilFault. |
ChunkLocation |
record |
(chunkX, chunkZ, region, dimension) — the single definition of the log context. |
AnvilChunkException must be declared non-sealed once it implements the sealed interface — a
compiler requirement, and correct in substance since downstream may subclass it.
-
Exactly one translation point from checked to unchecked: the catch in
FalcoAnvilLoader.loadChunk, and its counterpart insaveChunk. No other class may wrap checked into unchecked, or the origin is lost and the double report to theExceptionManagerreturns. -
Programmer errors keep the JDK types.
BitPacker,SectorAllocatorandPaletteData.singleValuekeep throwingIllegalArgumentException/IllegalStateException. Nobody catches them, they lead into no recovery path, and most are provably unreachable from the disk path. -
One exception to rule 2:
SectorAllocator.reserverejecting overlapping sectors is reachable from the disk path viaRegionFile.readHeader. It is corruption detection, not a programmer error, and is translated at theRegionFileboundary — without changing the allocator, which would cost it its file independence. -
Real IO stays
java.io.IOException. There is no explicit throw site for it in the package;FileChannel,Filesandchannel.forceproduce it implicitly. Wrapping adds no knowledge. -
No throw without a location. Every message from the format branch names at least the region
path and the chunk coordinate.
RegionFilealready does this;NbtReadsandPaletteDataname only the key or the number and must have the location passed in — otherwise the tick log readspalette must hold at least one entrywith no hint which file it came from. -
The context format is defined once, in
ChunkLocation.toString(). Exception messages must not repeat the coordinates as text, or today's double formatting persists. -
Never pass coordinates into
NbtReads,PaletteData,SectionCodec,BitPacker. These are deliberately format-only and testable without a running server. The location is attached at the loader boundary.
New package …anvil.exception with a package-info.java in the exact repository form
(@NotNullByDefault + package + import, four lines). Class Javadoc starting with The {@link X} is …
explaining the why, plus @author / @version / @since. Constructors (String) and
(String, Throwable); the (Throwable)-only constructor is deliberately omitted for the format
exceptions, because a format violation without a description is useless.
-
falco-anvil/src/main/java/net/onelitefeather/falco/anvil/— the 14 classes and thepackage-info.javathis design would cover, and the source of the 40throwsclauses, the two multi-catches inFalcoAnvilLoaderand the@since/ annotation facts above. Re-checked atca79507. - Why a failed read throws rather than reporting the chunk as absent — the design constraint this hierarchy has to preserve — is argued in Rationale: Chunk Loading.
- What the rest of these investigations are worth, and why their numbers are not benchmark numbers, is on Research.