A better way of exception handling in APIs

Smit Thakkar
5 min readMay 19, 2024

--

As a developer when you write a code something unexpected is bound to happen when you execute that code. When you are building a service, it’s important to handle these exceptions gracefully otherwise it fails to serve its promises made to clients.

When a client makes a request, there’s one happy path and N unhappy paths. A happy path is where a request is served successfully, as expected. unhappy path is where a request wasn’t fulfilled successfully due to numerous reasons. In this article, I discussed better ways of handling these unhappy scenarios.

Terms and Concepts

Exception: When a code is executing, something unexpected happens and the code throws an exception. It’s an exception to the normal execution of the code. Accessing an index greater than the array’s length causes IndexOutOfBoundsException in Java.

Client vs Server error — when a client invokes a server with a request, the server fails to serve because of multiple reasons. Some reasons are client errors and some reasons are server errors. If the client doesn’t have enough permissions for particular requests, it’s the client’s problem, not the server’s. But if the server is not able to serve due to a maintenance window, it’s the server’s fault.

Imperative vs Declarative programming: In Imperative programming, you write a code to describe how things should happen. In declarative, you write a code to describe what should happen, and how it happens is taken care of by the programming language and library.

Functional programming- It’s a style of programming where functions are chained together to create a flow of execution. It’s a declarative programming paradigm.

Exception handling flow

If you invoke a function that can throw an exception, it can disrupt the caller’s execution with those exceptions. The caller needs to handle those exceptions carefully and/or translate those exceptions into something meaningful.

try {
TurnOnFan();
} catch (e: Execption) {
if (e instanceof FanIsAlreadyOnException) {
// do nothing
} else if (e instanceof CommunicationException) {
// throw meaningful exception
throw RuntimeException("Could not reach fan, please check your connection");
}
}

Handling Unhappy Paths in API

When you write an API in a service, there are multiple dependency functions invoked within that API and each of them could fail to perform its expected execution. Some failures can be foreseen while writing code, while others are unexpected. Let’s take an example of a car reservation service, that has an API createReservation. Let’s see how things can go wrong when a client invokes this API.

createReservation(carType, time, paymentMethod)
- CarType is not valid
- Not enough permission to book carType
- Car is already reserved for that time.
- Payment is insufficient
- Network exceptions
- Timeout Exceptions

At this point, we are mainly interested in failures that are expected to happen due to client errors. All of these client errors should be handled carefully and the client should be made aware of these errors via appropriate status code and error values.

It is easy to miss some of these failures from the above list, and then those unhandled failures will translate into server errors, affecting your service availability.

When you invoke a method, you want to be sure that all failures from those methods are handled and are translated to the client appropriately.

Let’s talk about some ways to communicate these failures from a function to its caller.

Via exceptions

Each function either returns a successful response or throws an exception in case of failure. In that case, a caller of the function needs to wrap the function invocation in a try/catch and handle exceptions in the catch block.

Here’s the code of the createReservation function where each dependency function either throws an exception or returns a response.

class ReservationService {
fun createReservation(
username: String,
carType: String,
startTime: Long,
endTime: Long,
paymentDetails: PaymentDetails
): String {
try {
validateCarType(carType)
} catch (e: Exception) {
throw InvalidArgumentException("Invalid car type")
}

try {
validateTime(startTime)
} catch (e: Exception) {
throw InvalidArgumentException("Invalid start time")
}

try {
validateTime(endTime)
} catch (e: Exception) {
throw InvalidArgumentException("Invalid end type")
}

try {
validateStartTimeBeforeEndTime(startTime, endTime)
} catch (e: Exception) {
throw InvalidArgumentException("End time should be before than start time")
}

try {
validateReservationIsAllowed(username, carType)
} catch (e: Exception) {
throw ForbiddenException("$username not allowed to create reservation for $carType")
}

try {
return reserve(carType, startTime, endTime, paymentDetails)
} catch (e: Exception) {
if (e is InvalidPaymentMethod || e is InsufficientPayment) {
throw InvalidArgumentException("Payment details is not valid");
}
throw InternalException("Something went wrong")
}
}
}

class InternalException(msg: String): Exception("500: $msg")
class InvalidArgumentException(msg: String): Exception("400: $msg")
class ForbiddenException(msg: String): Exception("403: $msg")

Problems with exceptions

  • Difficult to read code: The try/catch block makes code very verbose and difficult to read and maintain. It’s difficult to understand the flow of the code with multiple exception handling.
  • Easy to miss exceptions from dependency functions: Exceptions are not part of the function definition. To handle all exceptions thrown by a function, the caller must look at the function’s implementation to understand which exceptions are thrown.
  • Exceptions affect performance: It’s costly to create and handle exceptions. When an exception is thrown it also attaches the stack trace with it, which inherits additional cost.

Via Either

Either is a data type in functional programming that combines two possible outcomes — success and error. Either has two parts: left and right. Usually left means error and right implies success. A caller of the function can work with left and right values of either.

A few examples of function definitions:

fun TurnOnFan(): Either<Error, Success>

fun getReservation(val reservationId: String):
Either<ReservationNotFoundError, Reservation>

The CreateReservation function will look like this if implemented using Either.

class ReservationService {
fun createReservation(
username: String,
carType: String,
startTime: Long,
endTime: Long,
paymentDetails: PaymentDetails
): Either<CreateReservationError, String> {
return validateCarType(carType).mapLeft {
it.toCreateReservationError("carType")
}.flatMap {
reserve(carType, startTime, endTime, paymentDetails).mapLeft { reserveError ->
reserveError.toCreateReservationError()
}
}
}
}

private fun validateCarType(carType: String): Either<ValidationError, Unit> {
TODO()
}

sealed class CreateReservationError(val code: Int, open val msg: String) {
data class InvalidArgumentError(override val msg: String): CreateReservationError(400, msg)
data class ForbiddenError(override val msg: String): CreateReservationError(403, msg)
data class InternalError(override val msg: String): CreateReservationError(500, msg)
}

fun ReserveError.toCreateReservationError(): CreateReservationError {
return when(this) {
is ReserveError.InsufficientPaymentError -> CreateReservationError.InvalidArgumentError("Insufficient payment")
is ReserveError.InvalidPaymentMethodError -> CreateReservationError.InvalidArgumentError("Invalid payment method")
}
}

sealed class ReserveError(open val msg: String) {
data class InvalidPaymentMethodError(override val msg: String): ReserveError(msg)
data class InsufficientPaymentError(override val msg: String): ReserveError(msg)
}

For brevity, I have omitted some validation checks in the above code. The above code is quite easy to read and understand the execution flow.

fun ReserveError.toCreateReservationError(): CreateReservationError

With the above function, it’s impossible to miss any errors thrown by ReserveError, thus making sure that all client errors are handled and translated appropriately.

Thank you for reading until the end.

Before you go:

  • Please consider clapping (👏) on this article.
  • Follow me on LinkedIn and Twitter for more ideas and resources like this.

References

--

--

Smit Thakkar
Smit Thakkar

Written by Smit Thakkar

Software Developer at DoorDash. Passionate about learning, sharing, building products and solving problems

No responses yet