Week 3 | Lesson 11

Handling Errors

Exceptions
Validations
Robustness

Exceptions

and error handling

What is an Exception

Exceptions are events that disrupt the normal flow of program execution.
  • They can arise due to various types of errors such as IO errors, arithmetic errors, null pointer access, etc.
  • Exception is just another type of Kotlin object:
    • Exception is an instance of a Exception class or one of its subclasses.
    • There are several subclasses of Exception provided in Kotlin by default, but we can create our own by extending these superclases.
    • There are two types of exceptions: Checked or Unchecked
  • The Exception object usually carries information about the error that occurred.
  • Exception handling allows us to control the program flow and prevent the program from terminating abruptly, which leads to a more robust and fault-tolerant software.

Checked Exceptions

These are exceptional conditions that a well-written application should anticipate and recover from.

Checked exceptions are the classes that extend Throwable class except RuntimeException and Error.

Checked exceptions are checked at compile-time. The compiler forces the programmer to catch these exceptions, i.e., the programmer needs to provide an exception handling mechanism through a try-catch block or throws keyword for checked exceptions. If not, the code will not compile.

For example, FileNotFoundException will be thrown when a file that needs to be opened cannot be found.

Unchecked Exceptions

These represent defects in the program (bugs), often invalid arguments passed to a non-private method.

Unchecked exceptions are the classes that extend RuntimeException class and the Error class.

Unchecked exceptions are not checked at compile-time, but at runtime.

Examples are ArrayIndexOutOfBoundsException, NullPointerException, ArithmeticException, NumberFormatException etc.

Handling exceptions

Kotlin provides a standard mechanisms to handle exceptions using try, catch, finally blocks.
						
							try {
								// code that might throw an exception
							} catch (ex: ExceptionType) {
								// code to handle the exception
							} finally {
								// code that will execute irrespective of an exception occurred or not
							}
						
					
  • The try block contains the code that might throw an exception.
  • The catch block contains the code that is executed when an exception of given type occurs in the try block.
  • The finally block contains the code that is always executed, regardless of whether an exception occurs or not.

Throwing exceptions

"Throwing an exception" refers to the process of creating an instance of an Exception (or its subclass) and handing it off to the runtime system to handle.

It's a way of signaling that a method cannot complete its normal computation due to some kind of exceptional condition.

There are two keywords associated with throwing exceptions:

  • The throw keyword is used to "emit" an exception from any block of code. We can throw either checked or unchecked exceptions.
  • If you want to declare that a method may throw an exception, you can use the @Throws annotation.
  • Declaring that a method throws an exception is a way of signaling to the caller that the method may not complete normally, so that the caller can handle it.

Throwing and handling exceptions

						
							fun main() {
								val car = Car(3)

								try {
									car.drive(4)
								} catch (e: NoFuelException) { // compiler will force catch block here
									println(e.message)
									// somehow handle car out of fuel situation
								}
							}
						
					
Class that throws exception
									
										 class Car(private var fuelKm: Int) {

											@Throws(NoFuelException::class)
											fun drive(driveKm: Int) {
												var driveKm = driveKm
												while (driveKm > 0) {
													if (fuelKm <= 0) {
														// exception in thrown on car out of fuel event
														throw NoFuelException()
													} else {
														println("drove 1 km")
														fuelKm--
														driveKm--
													}
												}
											}
										}
									
								
NoFuelException exception definition
									
										class NoFuelException : Throwable("The car is out of fuel!")
									
								

Running this code will print

									
										drove 1 km
										drove 1 km
										drove 1 km
										The car is out of fuel!
									
								

Throwing and handling exceptions

In this example we try to divide number by 0, which is illegal. The compiler will let us compile this code, because there is no checked exception. When executed, the program will end with:

Exception in thread "main" java.lang.ArithmeticException: / by zero
						
							fun main() {
								int number = 100 / 0; // will end with "Exception in thread "main" java.lang.ArithmeticException: / by zero"
							}
						
					

However, we can still handle the unchecked exception too, we are just not warned by the compiler.

						
							fun main() {
								val dividend = 100
								val divisor = 0

								try {
									val quotient = dividend / divisor
								} catch (e: Exception) {
									println(e.message)
								}
							}
						
					

Common Exceptions

Java and Kotlin provide a rich set of built-in exceptions that can be used to handle common error conditions.
  • NullPointerException
    Thrown when an application attempts to use null in a case where an object is required.

  • IllegalArgumentException
    Thrown to indicate that a method has been passed an illegal or inappropriate argument.

  • IllegalStateException
    Thrown to indicate that program reached an illegal state, such as illegal combination of parameters or illegal sequence of method calls.

  • IndexOutOfBoundsException
    Thrown to indicate that an index of some sort (such as an array or string) is out of range.

  • NumberFormatException
    Thrown when an attempt is made to convert a string to a numeric type, but the string does not have the appropriate format.

  • FileNotFoundException
    Thrown when an attempt to open the file denoted by a specified pathname has failed.

  • IOException
    Thrown when an I/O operation fails or is interrupted.

  • SQLException
    Thrown when an error occurs while accessing a database.

Exercise

Create a class OutOfFuelException that extends Throwable and sets the message to "Car is out of fuel."

Create a class Car with the following properties and methods:

  • private var fuelKm: Int
  • fun drive(distance: Int) that will check if car has enough fuel to drive the distance and reduce the fuelKm by the distance

Create an instance of the Car class and test the drive method with a distance that is greater than the fuelKm.

Use a try-catch-finally block to catch the OutOfFuelException and print the message.

Add another catch block to catch any other Exception and print the message.

Kotlin Validation

Kotlin Validation

Kotlin provides a way to validate data using the require and check functions.

These functions are a convenient way to throw exceptions when a condition is not met.

								
									val condition = a > b

									check(condition) { "Condition not met!" }
								
							
  • Validates that a condition is true.
  • If the condition is false, throws an IllegalStateException.
								
									val condition = a > b

									require(condition) { "Condition not met!" }
								
							
  • Validates that a condition is true.
  • If the condition is false, throws an IllegalArgumentException.
								
									val nullableValue: String? = null

									requireNotNull(nullableValue) { "Value must not be null!" }
								
							
  • Validates that a value is not null.
  • If the value is null, throws an IllegalArgumentException.

Usage Examples

These functions are often used to validate input parameters, state of an object, or any other condition that must be true for the program to continue executing.

For example, you can use require to validate that a list is not empty:

						
							fun processOrder(items: List<MenuItem>) {
								require(items.isNotEmpty()) { "Order must not be empty!" }
								// Order processing logic ...
							}
						
					
						
							@Serializable
							data class OrderRequest(
								val items: List<OrderItemRequest>
							) {
								init {
									require(items.isNotEmpty()) { "Order must not be empty!" }
								}
							}
						
					

Application Errors

General Concepts

Applications can encounter various types of errors during their execution. These errors can be due to user input, system state, or business logic violations.

So far, we have not paid great attention to the errors that can occur in our applications.

We know how to respond with different HTTP status codes base on expected service retuned values, but we didn't deal with error states that can occur in our applications.

Under normal operation, most application calls will not result in an exception. But there are valid reasons why an exception might be thrown.

  • Input validation
    For example, we may want to validate user inputs, such as valid JSON request body, or valid query parameter values. Example: non empty list, string, valid email address, etc.
  • State validation
    For example, we may want to validate the state of the application, such as whether a user is logged in, or whether a resource exists.
  • Business logic validation
    For example, we may want to validate the business logic of the application, such as whether a user has permission to perform an action, or whether a resource is available.

Defining Exceptions

It is often a good idea to define custom exceptions for specific error conditions in your application.

Defining custom exceptions allows you to provide more meaningful error messages and handle specific error conditions in a more granular way. It also allows you to treat same class of errors consistently across your application.

Here are few examples of programmer-defined exceptions:

  • InvalidCredentialsException
    Thrown when a user fails to authenticate.

  • UnauthorizedAccessException
    Thrown when a user is not authorized to perform an action.

  • ResourceNotFoundException
    Thrown when a requested resource is not found.

  • InvalidInputException
    Thrown when the input provided by the user is invalid.

  • ConflictException
    Thrown when the action would result in a conflict (duplicate).

Handling Exceptions

Handling exceptions individually is possible but impractical. Application frameworks usually provide a way to handle exceptions globally.

It is also a good idea to provide error responses in a standard format with a meaningful error message and maybe some additional information to help trace the error.

At the same time, it also a good idea to log all error messages in the application logs.

This can be later used to tie client errors to server logs.

Ktor allows us to use a StatusPages plugin to handle exceptions globally.

Using global error handling

In Ktor

To enable global error handling in Ktor, we can use the StatusPages plugin.

						
							install(StatusPages) {

								exception<BadRequestException> { call, cause ->
									call.respond(HttpStatusCode.BadRequest, mapOf("error" to cause.message))
								}

								exception<NotFoundException> { call, cause ->
									call.respond(HttpStatusCode.NotFound, mapOf("error" to cause.message))
								}

								exception<Throwable> { call, cause ->
									call.respond(HttpStatusCode.InternalServerError, mapOf("error" to "Something went wrong"))
								}

							}
						
					

Application error handling example

I have defined a few custom exceptions to handle common error conditions in my application. They all extend ApplicationException class, because I want all of them to contain a traceId and time properties.

						
							open class ApplicationException(
								val applicationMessage: String,
								val traceId: UUID = UUID.randomUUID(),
								val time: ZonedDateTime = ZonedDateTime.now(),
							) : RuntimeException("[$traceId] $applicationMessage")

							class UnauthorizedAccessException(message: String) : ApplicationException(message)

							class InvalidCredentialsException(userName: String) : ApplicationException("Login failed: $userName")

							class ResourceNotFoundException(message: String) : ApplicationException(message)
						
					

I have also defined convenience functions to throw these exceptions.

						
							fun unauthorizedAccess(message: String): Nothing {
								throw UnauthorizedAccessException(message)
							}

							fun menuItemNotFound(id: MenuItemId): Nothing {
								throw ResourceNotFoundException("Menu item with id: $id not found")
							}

							fun customerNotFound(userId: UserId): Nothing {
								throw ResourceNotFoundException("User $userId doesn't have a customer account")
							}
						
					

Application error handling example

Now I need to configure the StatusPages plugin to handle these exceptions globally.

						
							private val logger = KotlinLogging.logger {}

							fun StatusPagesConfig.configure() {

								exception<UnauthorizedAccessException> { call, cause ->
									logger.error(cause) { cause }
									call.respond(
										status = HttpStatusCode.Unauthorized,
										message = cause.toResponse()
									)
								}

								exception<ResourceNotFoundException> { call, cause ->
									logger.error(cause) { cause }
									call.respond(
										status = HttpStatusCode.NotFound,
										message = cause.toResponse()
									)
								}

								// Generic 400 error, we just add some additional information
								exception<BadRequestException> { call, cause ->
									logger.error(cause) { cause }
									call.respond(
										status = HttpStatusCode.BadRequest,
										message = ErrorResponse(
											traceId = UUID.randomUUID(),
											message = cause.message ?: "Unknown error occurred",
											time = ZonedDateTime.now()
										)
									)
								}

								// Generic 404 error, we just add some additional information
								exception<NotFoundException> { call, cause ->
									logger.error(cause) { cause }
									call.respond(
										status = HttpStatusCode.NotFound,
										message = ErrorResponse(
											traceId = UUID.randomUUID(),
											message = cause.message ?: "Unknown error occurred",
											time = ZonedDateTime.now()
										)
									)
								}

								// Generic 500 error, we just add some additional information
								exception<Throwable> { call, cause ->
									logger.error(cause) { cause }
									call.respond(
										status = HttpStatusCode.InternalServerError,
										message = ErrorResponse(
											traceId = UUID.randomUUID(),
											message = cause.message ?: "Unknown error occurred",
											time = ZonedDateTime.now()
										)
									)
								}
							}
						
					
						
							install(StatusPages) {
								configure()
							}
						
					

Application error handling example

Because I want application errors to be reported consistently and with enough information to trace the error in application logs, I have defined a common serializable ErrorResponse data class, and an extension function to convert my custom exceptions to this response object.

						
							@Serializable
								data class ErrorResponse(
								@Contextual
								val traceId: UUID,
								val message: String,
								@Contextual
								val time: ZonedDateTime,
							)

							fun ApplicationException.toResponse() = ErrorResponse(
								traceId = traceId,
								message = applicationMessage,
								time = time
							)
						
					

Application error handling example

In my services, whenever I recognize an error condition, I throw one of the custom exceptions and the StatusPages plugin will handle it globally.

						
							private val logger = KotlinLogging.logger {}

							class MenuService(
								private val menuRepository: MenuRepository
							) {

								suspend fun getMenuItem(id: MenuItemId): MenuItemResponse {
									logger.info { "Getting menu item with id: $id" }
									return menuRepository.selectMenuItemById(id)?.toResponse() ?: menuItemNotFound(id)
								}

								suspend fun createMenuItem(identity: IdentityDTO, request: MenuItemRequest): MenuItemResponse {
									logger.info { "Creating menu item: ${request.name} by user: ${identity.userId}" }
									return when (identity.role) {
										UserRole.STAFF -> menuRepository.insertMenuItem(request.toDTO()).toResponse()
										UserRole.CUSTOMER -> unauthorizedAccess("Only admin users can create menu items")
									}
								}
							}
						
					

Developing Resilient Applications

Developing Resilient Applications

Building robust backends means expecting failure and responding clearly, consistently, and safely.
  • Validate Early
    Use require, check, and null checks to catch errors before they propagate.

  • Fail Fast, Fail Loud
    Throw descriptive exceptions when invariants are broken.

  • Define Custom Exceptions
    Clarify intent and aid in debugging and response formatting.

  • Handle Exceptions Globally
    Centralize response behavior with StatusPages for consistency.

  • Log Errors with Context
    Include trace IDs and timestamps to help trace and debug issues.

  • Use Standard Error Responses
    Clients benefit from consistent, parseable error formats.

  • Test Failure Scenarios
    Robust apps are not those that never fail, but those that fail gracefully.

Next Lesson

Next Lesson

Application deployment and observability