Johann Pardanaud
03/01/2023, 3:55 PMval number = "123".toInt()
In the above code it is pretty clear that a failure ofThe solution is to usefunction is a bug in the code and so it throws atoInt
if the given string does not represent an integer. But what if someNumberFormatException
came from a user input field and application logic shall deal with invalid input by replacing it with a default value?string
val number = string.toIntOrNull() ?: defaultValue
, which is explained with:
It indicates an error through its result value, usingI understand the logic behind this, but later he writes the following function:in this particular case.null
fun updateOrderQuantity(orderId: OrderId, quantity: Int) {
require(quantity > 0) { "Quantity must be positive" }
val order = loadOrder(orderId)
order.quantity = quantity
storeOrder(order)
}
What if the quantity came from a user input? Should I write a new function updateOrderQuantityOrNull
? Is it idiomatic for a function to return a nullable value but to throw an exception on an I/O error?loadOrder
function does an HTTP request to retrieve the order and expects a JSON response with specific fields (the Order
class is serializable):
fun loadOrder(id: OrderId): Order {
httpClient.get("<https://example.com/orders/$id>").body()
}
Now consider that, for some reason, the server returns an incorrect JSON structure. How the loadOrder
function should handle this? Should we let kotlinx.serialization
throw an exception and catch it at the top of the application stack? Or should we return a null
value?Landry Norris
03/01/2023, 4:02 PMJacob
03/01/2023, 4:26 PMLandry Norris
03/01/2023, 4:27 PMJohann Pardanaud
03/01/2023, 4:28 PMLandry Norris
03/01/2023, 4:30 PMCasey Brooks
03/01/2023, 4:30 PMupdateOrderQuantity
being called, and if the quantity
were validated in the UI, would that fix the issue in that function?
Assuming updateOrderQuantity
is part of the “repository layer” of you app and is making some change (submitting an API call, updating the local DB, etc), then you should generally want to be pretty strict about your validation checks. Don’t trust anything passed in, make the check yourself. Even if the UI validates the quantity
in the UI and shows the user a nice error message, you still want the check so that things still get processed properly if that same function is called from some other point in your application, which may not have that same UI validation.
As for how exactly to model your “service calls”, it basically comes down to whether you want to set up your app to handle exceptions, or to branch the logic with result types (a null return value being one type of “result”, but you can use more explicit things like sealed classes, the built-in Result class, Arrow, etc.).
As a general rule, exceptions are easy to implement and pretty easy to handle (you can handle errors centrally without a bunch of intermediate steps) but are also easy to mess up or forget to handle. A centralized error-handler can also handle a bunch of different types of exceptions without necessarily needing to know the specific error that occurred (just show a simple message like “oops, something went wrong”).
Result types help force you to handle the errors, but can get a bit cumbersome as they need to be handled through all the intermediary layers between the UI and the service call. They give you a lot of insight into what went wrong, a lot of control on recovering from those errors, and generally a greater guarantee that you app is. behaving as you expect. But again, that comes at the cost of more boilerplate and needing to be more intentional about how you develop your app.Johann Pardanaud
03/01/2023, 4:47 PMLandry Norris
03/01/2023, 4:48 PMJohann Pardanaud
03/01/2023, 4:48 PMsealed class ExchangedCode {
object InvalidState : ExchangedCode()
data class Failure(val errorCode: String, val errorDescription: String?) : ExchangedCode()
data class Success(val accessToken: String) : ExchangedCode()
}
suspend fun exchangeCodeForAccessToken(code: OauthCode, state: OauthState): ExchangedCode {
if (!states.remove(state)) return ExchangedCode.InvalidState
val response = httpClient.get {
url.takeFrom(generateUrl())
}
val payload: Response = response.body()
if (payload.accessToken == null) {
return ExchangedCode.Failure(payload.error!!, payload.errorDescription)
}
return ExchangedCode.Success(payload.accessToken)
}
I really appreciate the fact I can easily handle the various cases with `when`:
when (val result = OauthClient.exchangeCodeForAccessToken(code, webSocketId)) {
Client.ExchangedCode.InvalidState -> call.respondText("Invalid or expired OAuth state.", status = HttpStatusCode.BadRequest)
is Client.ExchangedCode.Failure -> call.respondText("Code exchange failed: ${result.errorDescription ?: result.errorCode}", status = HttpStatusCode.BadRequest)
is Client.ExchangedCode.Success -> call.respondText("Here is your access token: ${result.accessToken}")
}