Result
Result[A] is Uni's standard container for "a value that may have failed". It is a Rust-flavored counterpart to scala.util.Try: the error channel is always a Throwable, and the combinators match Rx's vocabulary so interop is mechanical.
import wvlet.uni.util.ResultResult lives in uni-core and is available on the JVM, Scala.js, and Scala Native.
When to Use Result
Use Result[A] at API boundaries where failure is an expected outcome that callers need to handle as a value — parsers, external lookups, settled Rx evaluations — while keeping throw / try / catch inside internal code. Uni deliberately does not introduce a type-level error channel (like ZIO's E parameter): Scala's exception mechanism already carries the error type, and fixing E = Throwable keeps the shape single-parameter like Option[A] and Rx[A].
| Situation | Recommended type |
|---|---|
| Internal helper that can throw | Exceptions + NonFatal |
| Public API that distinguishes "failed" from "returned" | Result[A] |
| Async stream that produces zero or more values | Rx[A] |
Constructing a Result
Result.apply evaluates a block and wraps any non-fatal exception. Fatal errors (e.g. InterruptedException, VirtualMachineError) escape — this matches scala.util.control.NonFatal.
val parsed: Result[Int] = Result(Integer.parseInt("42"))
// Result.Success(42)
val failed: Result[Int] = Result(Integer.parseInt("not-a-number"))
// Result.Failure(java.lang.NumberFormatException: For input string: "not-a-number")Direct constructors and conversions are also available:
Result.success(1) // Result.Success(1)
Result.failure(new RuntimeException("no")) // Result.Failure(...)
Result.fromTry(scala.util.Try("parse this".toInt))
Result.fromEither(Right[Throwable, Int](42))
Result.fromOption(Option.empty[Int], new NoSuchElementException("empty"))Composing Results
map, flatMap, and filter work like any other monadic container, and they catch any exceptions thrown inside the supplied function, turning them into Failure. This is the "Rx-like propagation" property: exceptions never bypass the wrapper.
val r: Result[Int] =
for
a <- Result(Integer.parseInt("10"))
b <- Result(Integer.parseInt("20"))
if a + b > 0
yield a + b
// Result.Success(30)A Failure anywhere in the chain short-circuits the rest — no need to thread the error manually.
Handling Failure
recover and recoverWith mirror the same-named methods on Rx and only fire when the partial function matches the wrapped throwable:
val recovered: Result[Int] =
Result(Integer.parseInt("nope")).recover {
case _: NumberFormatException => 0
}
// Result.Success(0)
val retried: Result[Int] =
Result.failure(new java.io.IOException("timeout")).recoverWith {
case _: java.io.IOException => Result(42)
}
// Result.Success(42)Other handlers:
Result.failure(boom).mapError(new IllegalStateException(_)) // rewrap
result.fold(onFailure = _.getMessage, onSuccess = _.toString)
result.getOrElse(0)
result.orElse(Result.success(0))Conversions
Result interoperates with the standard Scala types:
result.toOption // Option[A] — Failure becomes None
result.toEither // Either[Throwable, A]
result.toTry // scala.util.Try[A]
result.get // A; rethrows on FailureRx Interop
Rx[A].materialize reifies each upstream event as a Result[A], so errors are delivered as values instead of terminating the stream:
import wvlet.uni.rx.Rx
val events: Rx[Result[Int]] =
Rx.exception[Int](new RuntimeException("boom")).materialize
// Emits Result.Failure(RuntimeException("boom"))To go the other way, use Rx.fromResult (or the toRx extension on Result, available after importing wvlet.uni.rx.*):
import wvlet.uni.rx.*
val rx1: Rx[Int] = Rx.fromResult(Result.success(1))
val rx2: Rx[Int] = Result.success(1).toRxResult.Failure(e) converts to an Rx that emits OnError(e) — the same termination signal a thrown exception would produce.
