Skip to content

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.

scala
import wvlet.uni.util.Result

Result 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].

SituationRecommended type
Internal helper that can throwExceptions + NonFatal
Public API that distinguishes "failed" from "returned"Result[A]
Async stream that produces zero or more valuesRx[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.

scala
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:

scala
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.

scala
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:

scala
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:

scala
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:

scala
result.toOption     // Option[A] — Failure becomes None
result.toEither     // Either[Throwable, A]
result.toTry        // scala.util.Try[A]
result.get          // A; rethrows on Failure

Rx Interop ​

Rx[A].materialize reifies each upstream event as a Result[A], so errors are delivered as values instead of terminating the stream:

scala
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.*):

scala
import wvlet.uni.rx.*

val rx1: Rx[Int] = Rx.fromResult(Result.success(1))
val rx2: Rx[Int] = Result.success(1).toRx

Result.Failure(e) converts to an Rx that emits OnError(e) — the same termination signal a thrown exception would produce.

Released under the Apache 2.0 License.