java.lang.Object
io.github.brantunger.unruly.api.language.Throwables

public final class Throwables extends Object
Reads what an expression language, a rule or the application's code threw, as the engine reads it: which errors the engine rethrows unchanged, what a stack overflow looks like once the JDK or a library has wrapped it, and the message and causes of an exception the language didn't create, whose getMessage() or getCause() may throw. It delegates to the engine's own readers rather than keeping a copy of them, so a language that catches what it calls decides as the engine decides: a warm-up that must skip an overflow but rethrow a class left unusable by one (see ExpressionLanguage.prepare()), an action registered with EvaluationContext.onCancel(java.lang.Runnable) that must let a fatal error through, or a class lookup that must read the JVM's "wrong name" error as "not found".

Every method here takes what it reads and returns a plain answer, with nothing retained: the class is stateless, and its methods are safe to call from any thread at once. isOverflow(java.lang.Throwable) and isFatal(java.lang.Throwable) allocate nothing and link no call site, so they can be called where handling a failure has itself failed, deep in a stack or with no memory left. Each is fixed for 2.x: what it reads, in what order, how far, and what it answers for null.

  • Method Details

    • isOverflow

      public static boolean isOverflow(@Nullable Throwable thrown)
      Tells whether a throwable is a StackOverflowError, or what was thrown in place of one: walking its cause chain from the top, as causeChain(java.lang.Throwable) walks it, a StackOverflowError comes before any LinkageError. A NoClassDefFoundError or an ExceptionInInitializerError above the overflow says a class was left unusable, by an overflow in its static initializer, maybe in an earlier load, which no change to the rule or the stack's depth helps, as JDK 25 and later make one with the overflow as its cause; so that isn't an overflow. An InternalError around one, as the JDK wraps an overflow it hits defining a hidden class, is. The engine reports an overflow as the rule's failure, with more stack as the fix, and never as a fatal error (see isFatal(java.lang.Throwable)).

      The chain is read link by link, at most 100 links, a getCause() that throws ending it, with nothing allocated and no call site linked, so it can be asked where handling a failure has itself failed. A chain that loops back on itself is read as its first 100 links.

      Parameters:
      thrown - What was caught, or null, which isn't an overflow
      Returns:
      true if a StackOverflowError is in its cause chain with no LinkageError before it
    • isFatal

      public static boolean isFatal(@Nullable Throwable thrown)
      Tells whether a throwable is an Error the engine must not absorb, because the JVM itself is failing and no message about a rule would help: a VirtualMachineError other than StackOverflowError, such as OutOfMemoryError, InternalError or UnknownError, but not an InternalError that wraps an overflow (see isOverflow(java.lang.Throwable)). The engine rethrows such an error unchanged from run(), load(), validate() and close(), even when a language wraps it or suppresses it in an exception (see fatalError(java.lang.Throwable)), and reports every other error, an AssertionError or a LinkageError included, as the failure of the rule, listener or language it came from.

      Only the throwable itself is read, not its causes: it allocates nothing and links no call site, so it can be asked where handling a failure has itself failed, deep in a stack or with no memory left. Only an InternalError has its cause chain read, as isOverflow(java.lang.Throwable) reads it.

      Parameters:
      thrown - What was caught, or null, which isn't fatal
      Returns:
      true if the engine would rethrow thrown unchanged
    • fatalError

      public static @Nullable Error fatalError(@Nullable Throwable thrown)
      Finds the fatal error (see isFatal(java.lang.Throwable)) in what was caught, as the engine finds the one it rethrows: the throwable itself, or one of its causes, or failing that one suppressed on any of them, or among the causes and suppressed exceptions of those, at any depth, as a try-with-resources whose close() hits an OutOfMemoryError while its body throws leaves it. The throwable's own cause chain is read first, as causeChain(java.lang.Throwable) reads it, at most 100 links, so a fatal error further down it, or suppressed on a link further down, is missed; then the direct suppressed exceptions of each of those links, from the top link down, before what any of them leads to, depth first: a suppressed exception's cause chain is read in full, link by link, with no limit of its own, then the suppressed exceptions of its links the same way. Each exception is read once, by identity, and at most 10,000 are, so a fatal error past that many is missed, as the engine misses it. A suppressed exception the engine itself added to a failure it reported, beside the one it rethrows, isn't read: the engine keeps there a fatal error a listener threw, and finding it again would take it for the failure's own.

      It allocates the list and the identity set of causeChain(java.lang.Throwable), and a walk when any link has suppressed exceptions.

      Parameters:
      thrown - What was caught, or null, which holds no fatal error
      Returns:
      The first fatal error found in that order, or null if there is none among the exceptions read
    • isInterrupt

      public static boolean isInterrupt(@Nullable Throwable thrown)
      Tells whether what was caught was caused by an interrupt, or carries one: an InterruptedException is the throwable itself, one of its causes, or suppressed on any of them, or among the causes and suppressed exceptions of those, read as fatalError(java.lang.Throwable) reads them, in the same order, with the same limits, and skipping what the engine itself added. A rule, listener or language interrupted while it blocks throws an InterruptedException, which clears the thread's interrupt status, and the engine receives it wrapped by the language, or suppressed on the body's exception by a try-with-resources whose close() was interrupted; the engine sets the status again when it finds one, so the caller of run() can tell. A InterruptedIOException doesn't count: it isn't an InterruptedException.

      It allocates what fatalError(java.lang.Throwable) allocates.

      Parameters:
      thrown - What was caught, or null, which holds no interrupt
      Returns:
      true if an InterruptedException is among the exceptions read
    • messageOf

      public static @Nullable String messageOf(Throwable thrown)
      Reads an exception's message without letting a failure to read it escape. A getMessage() of the application's own can throw, such as one built from a field that is null, and a failure being handled would then end as that one. Whatever getMessage() throws, a fatal Error too, only makes the message unavailable: it says nothing about the failure being handled. The accessor is called in a try of its own rather than through a lambda or method reference, so the JVM's first failure links no call site, which could overflow deep in a stack. It allocates only when getMessage() throws.
      Parameters:
      thrown - The exception
      Returns:
      Its message, or null if it has none, or (message unavailable: ...), naming the class of what getMessage() threw, if it can't be read
      Throws:
      NullPointerException - if thrown is null
    • causeChain

      public static List<Throwable> causeChain(@Nullable Throwable thrown)
      Lists an exception and its causes, from the top: the exception, its cause, its cause's cause and so on, each read as messageOf(java.lang.Throwable) reads a message, so a getCause() that throws ends the chain there. The chain also stops before a link it holds already, by identity, as a chain that loops back on itself has one, and after 100 links, as a getCause() of its own that returns a new exception every time would otherwise make a chain that never ends. The engine reads a throwable's own cause chain this way wherever it describes a failure or looks for a fatal error in one; a chain under a suppressed exception has no limit of its own (see fatalError(java.lang.Throwable)).

      It allocates a new list, which the caller owns and may change, and an identity set while it reads.

      Parameters:
      thrown - The exception, or null, whose chain is empty
      Returns:
      A new list of thrown and the causes that could be read, in order, which is empty only for null
    • rootCause

      public static Throwable rootCause(Throwable thrown)
      Finds the last exception in an exception's cause chain, as causeChain(java.lang.Throwable) reads it: the exception itself if it has no cause, or the last cause that could be read, which the engine names in a message when the description would otherwise hide it. It allocates what causeChain(java.lang.Throwable) allocates.
      Parameters:
      thrown - The exception
      Returns:
      The last link of its cause chain
      Throws:
      NullPointerException - if thrown is null
    • isWrongName

      public static boolean isWrongName(@Nullable Throwable error, String name)
      Tells whether an error a class lookup threw only means that a class file was found for the name looked up in a different case, as a class directory on a case-insensitive file system finds one, so there is no class by that name: the JVM's "wrong name" NoClassDefFoundError. Its message names the class asked for, in internal form (com/acme/Rule), on one side, and only the side this JVM writes it on is read: first on HotSpot, as in com/acme/rule (wrong name: com/acme/Rule), and last on OpenJ9, as in ... (wrong name: com/acme/rule), told by its java.vm.name. An error about a class the one looked up depends on, such as its superclass, doesn't name it on that side, so it isn't taken for it. That holds even for a superclass whose name differs from the class's only in case, whose lookup finds the class's own file: HotSpot's error for com.acme.Rule's superclass com.acme.rule is com/acme/rule (wrong name: com/acme/Rule), which names com.acme.Rule last. Nor is any other error taken for it, or one whose message can't be read (see messageOf(java.lang.Throwable)). The engine treats a lookup that threw such an error as a class that isn't there, and tries the next form of the name.

      HotSpot cuts the message at 1023 bytes of UTF-8, so for a name over about 1,000 ASCII characters, or 500 two-byte ones, " (wrong name: " is cut off and the error isn't recognised: the class then can't be loaded rather than being missing, an error, never a wrong answer. ImportLimits.MAX_IMPORT_LENGTH keeps an ASCII import under the cut. For a NoClassDefFoundError, it allocates the internal form of a name with dots, the strings it compares the message with, and what messageOf(java.lang.Throwable) allocates when the message can't be read; for any other error, nothing.

      Parameters:
      error - What the lookup threw, or null, which isn't the error
      name - The binary name the lookup asked for, with dots, as com.acme.Rule or com.acme.Rule$Nested
      Returns:
      true for the JVM's "wrong name" NoClassDefFoundError for name
      Throws:
      NullPointerException - if name is null
    • isMissingRegistration

      public static boolean isMissingRegistration(@Nullable Throwable error)
      Tells whether an error is the one a native image built with strict reachability metadata throws for a class or method lookup it has no metadata for, existing or not, where any other build, and the JVM, throw ClassNotFoundException or NoSuchMethodException, or list what is there: GraalVM's org.graalvm.nativeimage.MissingReflectionRegistrationError, an Error in GraalVM for JDK 21 and a LinkageError in GraalVM for JDK 25. It is told by its class's name, so neither the engine nor a language needs a dependency on GraalVM's SDK, and no other error, a StackOverflowError least of all, is taken for it. The engine treats such an error as "not found", so a package import still works in an image. It allocates nothing but the name of the error's class, the first time the JVM is asked for it.
      Parameters:
      error - What the lookup threw, or null, which isn't the error
      Returns:
      true for GraalVM's MissingReflectionRegistrationError