Error Handling
Spice Framework provides a comprehensive, type-safe error handling system that eliminates try-catch hell and makes error handling explicit and composable.
Overviewβ
The error handling system consists of two main components:
SpiceResult<T>- A sealed class representing either success or failureSpiceError- A hierarchy of typed errors for different failure scenarios
Why SpiceResult?β
Traditional error handling in Kotlin has several problems:
// β Problem 1: Exceptions are invisible in type signatures
fun processData(): String {
throw Exception("Failed!") // Caller has no idea this can fail
}
// β Problem 2: Null doesn't provide error context
fun findUser(id: String): User? {
return null // Why did it fail? We don't know!
}
// β Problem 3: Try-catch hell
try {
val data = fetchData()
try {
val processed = processData(data)
try {
saveData(processed)
} catch (e: IOException) {
// Handle save error
}
} catch (e: ValidationException) {
// Handle validation error
}
} catch (e: NetworkException) {
// Handle network error
}
SpiceResult solves all of these:
// β
Errors are explicit in the type signature
fun processData(): SpiceResult<String>
// β
Errors carry context
SpiceError.ValidationError(
message = "Invalid email format",
field = "email",
expectedType = "email",
actualValue = "not-an-email"
)
// β
Composable error handling
fetchData()
.flatMap { processData(it) }
.flatMap { saveData(it) }
.recover { error -> /* Handle any error */ }
Key Featuresβ
1. Railway-Oriented Programmingβ
SpiceResult implements the Railway-Oriented Programming pattern, where operations either stay on the "success track" or switch to the "failure track":
SpiceResult.success(10)
.map { it * 2 } // Success: 20
.flatMap { divide(it, 5) } // Success: 4
.map { it + 1 } // Success: 5
.getOrElse(0) // Returns: 5
SpiceResult.success(10)
.map { it * 2 } // Success: 20
.flatMap { divide(it, 0) } // Failure: Division by zero
.map { it + 1 } // Skipped (still Failure)
.getOrElse(0) // Returns: 0 (default)
2. Typed Error Hierarchyβ
11 specialized error types provide context for different failure scenarios:
sealed class SpiceError {
data class AgentError(...)
data class CommError(...)
data class ToolError(...)
data class ConfigurationError(...)
data class ValidationError(...)
data class NetworkError(...)
data class TimeoutError(...)
data class AuthenticationError(...)
data class RateLimitError(...)
data class SerializationError(...)
data class UnknownError(...)
}
3. Functional Operatorsβ
Powerful operators for transforming and recovering from errors:
map- Transform success valuesflatMap- Chain operations that return Resultsrecover- Recover from errors with a default valuerecoverWith- Recover from errors with another Resultfold- Handle both success and failure casesonSuccess/onFailure- Side effectsgetOrElse- Get value or defaultgetOrThrow- Get value or throw exception
4. Async Supportβ
First-class support for coroutines and Flow:
// Catching suspend functions
SpiceResult.catchingSuspend {
delay(100)
fetchDataFromAPI()
}
// Flow integration
flow { emit(data) }
.asResult() // Convert to Flow<SpiceResult<T>>
.filterSuccesses() // Only emit successful values
Quick Startβ
Basic Usageβ
import io.github.noailabs.spice.error.*
// Create results
val success = SpiceResult.success("Hello")
val failure = SpiceResult.failure<String>(
SpiceError.validationError("Invalid input")
)
// Check result
when (result) {
is SpiceResult.Success -> println("Value: ${result.value}")
is SpiceResult.Failure -> println("Error: ${result.error.message}")
}
With Agentsβ
import io.github.noailabs.spice.SpiceMessage
import io.github.noailabs.spice.springboot.ai.factory.SpringAIAgentFactory
val factory: SpringAIAgentFactory = ... // Inject
val agent = factory.anthropic("claude-3-5-sonnet-20241022")
val message = SpiceMessage.create("Hello", "user")
agent.processMessage(message)
.map { it.content.uppercase() }
.recover { error ->
SpiceMessage.create("Fallback response", "system")
}
.onSuccess { println("Success: ${it.content}") }
.onFailure { error -> logger.error("Failed: ${error.message}") }
Error Recoveryβ
fun fetchUserData(userId: String): SpiceResult<User> {
return SpiceResult.catching {
apiClient.getUser(userId)
}.recoverWith { error ->
when (error) {
is SpiceError.NetworkError -> {
// Try cache
cacheClient.getUser(userId)
?.let { SpiceResult.success(it) }
?: SpiceResult.failure(error)
}
is SpiceError.RateLimitError -> {
// Wait and retry
delay(error.retryAfterMs ?: 1000)
fetchUserData(userId)
}
else -> SpiceResult.failure(error)
}
}
}
Next Stepsβ
- SpiceResult Guide - Detailed SpiceResult API
- SpiceError Types - All error types explained
- Best Practices - Error handling patterns