User Guide

kotlin-throwable-policy is for containment boundaries: places that catch broadly so one unit of work does not terminate a process, but where abort signals and fatal errors still must not be buried.

Propagation Boundary

import one.wabbit.throwables.Throwables

fun handleRequest(block: () -> Unit): Result<Unit> =
    try {
        block()
        Result.success(Unit)
    } catch (t: Throwable) {
        Throwables.propagateIfNeeded(t)
        Result.failure(t)
    }

propagateIfNeeded delegates to mustPropagate. When it finds a throwable that the policy says must propagate, it rethrows that throwable.

Propagation precedence is:

  1. fatal platform errors
  2. interruption or cancellation
  3. non-fatal errors
  4. non-cancellation control flow
  5. contract violations

Classification

Use classification helpers when you need a decision without throwing:

val verdict = Throwables.isCancellation(t)

if (verdict.isYes) {
    // Treat as cooperative cancellation, not an application error.
}

Verdict.UNKNOWN means traversal reached the configured depth limit before the category could be ruled out.

isControlFlow treats interruption and cancellation as higher-priority abort signals. If either is found, it returns Verdict.NO for control flow.

Policy Modes

The default policy is conservative about allocation. It follows the preferred cause chain and common wrapper exceptions, but it does not scan suppressed exceptions.

Use a custom or strict policy when correctness against complex throwable graphs matters more than allocation avoidance:

val policy = Throwables.defaultPolicy.copy(
    allowAllocations = true,
    scanSuppressed = true,
)

scanSuppressed = true requires allowAllocations = true.

The rethrowControlFlow, rethrowCancellations, rethrowInterrupts, and rethrowContractViolations flags decide which non-fatal categories mustPropagate returns. Fatal platform errors always propagate regardless of those flags.

Suppressed Failures

Use handleSuppressed when cleanup fails while another failure is already primary:

try {
    cleanup()
} catch (suppressed: Throwable) {
    Throwables.handleSuppressed(primary, suppressed)
}

If the cleanup failure is a higher-priority signal, it is thrown and the primary failure is attached as suppressed on a best-effort basis.

When restoreInterrupt is enabled, handleSuppressed restores the interrupt flag if the cleanup failure contains an interruption signal.