Safe

Safe catches non-fatal exceptions thrown by the inner stage and converts them into Status.Complete error values, widening the error type from E to Either[Throwable, E].

Fatal exceptions (those not matched by scala.util.control.NonFatal) propagate normally. The evolution is mapped so that every continuation stage remains wrapped in Safe.

Safe is a crutch for the simplest cases — stateless stages. Per the Lifecycle contract, a stage that throws from apply has already released its own resources, so the recovered continuation simply reuses the inner stage — retrying it on the next input — and carries a no-op dispose. A stage that owns resources should be made safe by itself instead of being wrapped: extend SafeStage and supply a recover with the appropriate cleanup (the ConstEvolution overload taking a dispose function exists for exactly this).

import h8io.stages.*
import h8io.stages.base.*
import h8io.stages.operators.*
object ParseIntUnsafe extends SAMStage[String, Int, Nothing] {
  override def apply(in: String): Yield[String, Int, Nothing] =
    Yield.Some(in.toInt, Status.Success, this)
}

val safe = Safe(ParseIntUnsafe)
// safe: Safe[String, Int, Nothing] = Safe(alterand = <function1>)
safe("42")
// res0: Yield[String, Int, Either[Throwable, Nothing]] = Some(
//   out = 42,
//   status = Success,
//   evolution = Mapped(
//     evolution = <function1>,
//     f = h8io.stages.operators.Safe$$Lambda$19908/0x00007f58029c4000@1422e059
//   )
// )
safe("hello")
// res1: Yield[String, Int, Either[Throwable, Nothing]] = None(
//   status = Complete(
//     Left(value = java.lang.NumberFormatException: For input string: "hello")
//   ),
//   evolution = ConstEvolution(
//     stage = Safe(alterand = <function1>),
//     _dispose = h8io.stages.base.ConstEvolution$$$Lambda$19765/0x00007f5802976ab0@117a8253
//   )
// )