Skip to main content

Result Types

Archer supports multiple result types to handle success and failure scenarios. All result types use Arrow's typed errors foundation.

Ice (Idle | Content | Error)

Ice is a three-state result type unique to Archer, perfect for UI states.

sealed class Ice<out E, out A> {
data object Idle : Ice<Nothing, Nothing>
data class Content<out A>(val value: A) : Ice<Nothing, A>
data class Error<out E>(val error: E) : Ice<E, Nothing>
}

Usage

val result: Ice<DomainError, User> = ice {
repository.get(StoreSync.StoreFirst, userId)
}

when (result) {
is Ice.Idle -> showLoadingState()
is Ice.Content -> showUser(result.value)
is Ice.Error -> showError(result.error)
}

Why Ice?

Ice is particularly useful for UI state management:

class UserViewModel {
private val _userState = MutableStateFlow<Ice<UserError, User>>(Ice.Idle)
val userState: StateFlow<Ice<UserError, User>> = _userState

fun loadUser(userId: Int) {
viewModelScope.launch {
_userState.value = Ice.Idle // Show loading

_userState.value = ice {
repository.get(StoreSync.StoreFirst, userId)
}
}
}
}

Ice Operations

fold

Handle all three states:

result.fold(
ifIdle = { showLoading() },
ifContent = { user -> showUser(user) },
ifError = { error -> showError(error) }
)

getOrNull

Extract value or return null:

val user: User? = result.getOrNull()

getOrElse

Provide a default value:

val user: User = result.getOrElse { User.DEFAULT }

map

Transform the content:

val userName: Ice<UserError, String> = userIce.map { it.name }

Either (Left | Right)

Either is Arrow's classic functional error handling type.

sealed class Either<out A, out B> {
data class Left<out A>(val value: A) : Either<A, Nothing>()
data class Right<out B>(val value: B) : Either<Nothing, B>()
}

Usage

val result: Either<DomainError, User> = either {
repository.get(StoreSync.StoreFirst, userId)
}

when (result) {
is Either.Left -> handleError(result.value)
is Either.Right -> handleSuccess(result.value)
}

Either Operations

fold

val message = result.fold(
ifLeft = { error -> "Error: ${error.message}" },
ifRight = { user -> "Hello, ${user.name}" }
)

getOrNull

val user: User? = result.getOrNull()

getOrElse

val user: User = result.getOrElse { User.ANONYMOUS }

map

val userName: Either<UserError, String> = userEither.map { it.name }

mapLeft

Transform the error:

val result: Either<String, User> = userEither.mapLeft { error ->
"Failed to load user: ${error.message}"
}

Nullable

The simplest result type - just a nullable value.

Usage

val user: User? = nullable {
repository.get(StoreSync.StoreFirst, userId)
}

if (user != null) {
showUser(user)
} else {
showError()
}

When to Use Nullable

  • Simple operations where you don't need error details
  • Prototyping or quick scripts
  • When integrating with nullable-based APIs

Choosing a Result Type

Use Ice when:

  • Building UI with loading/content/error states
  • You need to represent "not yet loaded" separately from errors
  • Working with state management (MVI, MVVM)

Use Either when:

  • You need detailed error information
  • Writing functional code with Arrow
  • Composing multiple fallible operations
  • You want compile-time guarantees for error handling

Use Nullable when:

  • Errors don't need specific handling
  • Integrating with Java/nullable-based code
  • Quick scripts or prototypes
  • Simple existence checks

Raising Errors

All result types use Arrow's raise mechanism:

val result = ice {
val user = repository.get(StoreSync.StoreFirst, userId)

if (!user.isActive) {
raise(UserNotActive(userId))
}

user
}

Custom Error Types

Define your domain errors:

sealed interface UserError : DomainError {
data class NotFound(val userId: Int) : UserError
data class NotActive(val userId: Int) : UserError
data class InvalidData(val reason: String) : UserError
}

sealed interface NetworkError : DomainError {
data class Timeout(val url: String) : NetworkError
data class Unauthorized(val endpoint: String) : NetworkError
data class ServerError(val code: Int, val message: String) : NetworkError
}

Use them in your DataSources:

val userDataSource = getDataSource<Int, User> { userId ->
try {
val response = api.getUser(userId)

when {
response.code == 404 -> raise(UserError.NotFound(userId))
response.code == 401 -> raise(NetworkError.Unauthorized("/users/$userId"))
!response.isSuccessful -> raise(
NetworkError.ServerError(response.code, response.message)
)
else -> response.body.toDomain()
}
} catch (e: TimeoutException) {
raise(NetworkError.Timeout("/users/$userId"))
}
}

Error Recovery

With Either

val user: Either<UserError, User> = either {
repository.get(StoreSync.StoreFirst, userId)
}.recover { error ->
when (error) {
is UserError.NotFound -> User.ANONYMOUS
is UserError.NotActive -> User.GUEST
else -> raise(error)
}
}

With Ice

val user: Ice<UserError, User> = ice {
repository.get(StoreSync.StoreFirst, userId)
}.recover { error ->
if (error is UserError.NotFound) {
User.ANONYMOUS
} else {
raise(error)
}
}

Combining Results

Sequential Operations

val result = either {
val user = userRepository.get(StoreSync.StoreFirst, userId).bind()
val profile = profileRepository.get(StoreSync.StoreFirst, user.profileId).bind()
val preferences = prefsRepository.get(StoreSync.StoreFirst, user.id).bind()

UserData(user, profile, preferences)
}

Parallel Operations

suspend fun loadUserData(userId: Int): Either<AppError, UserDashboard> = either {
coroutineScope {
val userDeferred = async { userRepository.get(StoreSync.StoreFirst, userId) }
val postsDeferred = async { postsRepository.get(StoreSync.StoreFirst, userId) }
val followersDeferred = async { followersRepository.get(StoreSync.StoreFirst, userId) }

UserDashboard(
user = userDeferred.await().bind(),
posts = postsDeferred.await().bind(),
followers = followersDeferred.await().bind()
)
}
}

Best Practices

1. Use Typed Errors

// Good: Specific error types
sealed interface UserError : DomainError {
data class NotFound(val id: Int) : UserError
data class Unauthorized(val id: Int) : UserError
}

// Avoid: Generic exceptions
throw Exception("User not found")

2. Handle Errors at the Right Level

// Good: Handle at UI layer
viewModelScope.launch {
userRepository.get(userId).fold(
ifLeft = { error -> showError(error) },
ifRight = { user -> showUser(user) }
)
}

// Avoid: Catching too early
val user = try {
repository.get(userId)
} catch (e: Exception) {
null // Lost error information
}

3. Provide Meaningful Error Messages

sealed interface UserError : DomainError {
val message: String

data class NotFound(val userId: Int) : UserError {
override val message = "User with ID $userId was not found"
}

data class InvalidEmail(val email: String) : UserError {
override val message = "Email '$email' is not valid"
}
}

Next Steps

  • Examples - See practical examples
  • Recipes - Common patterns and solutions