Skip to main content
Version: 2.x

Use Union Types to Be More Specific About Error Types

In Scala 3, we have an exciting new feature called union types. By using the union operator, we can encode multiple error types. Using this facility, we can have more precise information on typed errors.

Let's see an example of Storage service which have upload, download and delete API:

import zio._

type Name = String

enum StorageError extends Exception {
case ObjectExist(name: Name) extends StorageError
case ObjectNotExist(name: Name) extends StorageError
case PermissionDenied(cause: String) extends StorageError
case StorageLimitExceeded(limit: Int) extends StorageError
case BandwidthLimitExceeded(limit: Int) extends StorageError
}

import StorageError.*

trait Storage {
def upload(
name: Name,
obj: Array[Byte]
): ZIO[Any, ObjectExist | StorageLimitExceeded, Unit]

def download(
name: Name
): ZIO[Any, ObjectNotExist | BandwidthLimitExceeded, Array[Byte]]

def delete(name: Name): ZIO[Any, ObjectNotExist | PermissionDenied, Unit]
}

Union types allow us to get rid of the requirement to extend some sort of common error types like Exception or Throwable. This allows us to have completely unrelated error types.

In the following example, the FooError and BarError are two distinct error. They have no super common type like FooBarError and also they are not extending Exception or Throwable classes:

import zio.*

// Two unrelated errors without having a common supertype
trait FooError
trait BarError

def foo: IO[FooError, Nothing] = ZIO.fail(new FooError {})
def bar: IO[BarError, Nothing] = ZIO.fail(new BarError {})

val myApp: ZIO[Any, FooError | BarError, Unit] = for {
_ <- foo
_ <- bar
} yield ()