Share
Explore

Exception Handling in Kotlin

This lab workbook provides a comprehensive introduction to exception handling in Kotlin, with progressively complex exercises and real-world examples. The drills build upon each other and cover different aspects of exception handling.

Summary of Key Learning Topics of this:
Make it a part of your professional practices as as programmer, to put exception handling at the center of your software designs.
Wrap up your code in try catch (finally) block wherever applicable.
Decide to be the in house expert in creating and applying custom exception classes → this has the side effect of making your code more robust and supportable.


Learning Objectives
By completing this lab, you will: 1. Understand the purpose and importance of exception handling 2. Learn different types of exceptions in Kotlin 3. Master try-catch-finally blocks 4. Implement custom exceptions 5. Practice defensive programming techniques

Part 1: Understanding Exceptions
Exceptions are unexpected events that occur during program execution.
They represent error conditions that need to be handled to prevent program crashes.
#### Basic Structure: ```kotlin try { // Code that might throw an exception } catch (e: Exception) { // Code to handle the exception } finally { // Code that always executes } ```
#### Common Exception Types: - `NullPointerException` - `IllegalArgumentException` - `IndexOutOfBoundsException` - NumberFormatException - `IOException`
### Part 2: Progressive Code Drills
#### Drill 1: Basic Exception Handling Create a simple calculator function that handles division by zero.
fun divide(a: Int, b: Int): Double { try { return a.toDouble() / b } catch (e: ArithmeticException) { println("Error: Cannot divide by zero") return 0.0 } }
fun main() { println(divide(10, 2)) // Should print: 5.0 println(divide(10, 0)) // Should print: Error message and 0.0 } ```
Drill 2: Multiple Exception Types Create a function that converts string input to numbers and performs calculations.
```kotlin fun calculateFromString(num1: String, num2: String): Double { try { val number1 = num1.toDouble() val number2 = num2.toDouble() if (number2 == 0.0) { throw ArithmeticException("Division by zero is not allowed") } return number1 / number2 } catch (e: NumberFormatException) { println("Error: Please enter valid numbers") return 0.0 } catch (e: ArithmeticException) { println("Error: ${e.message}") return 0.0 } catch (e: Exception) { println("An unexpected error occurred: ${e.message}") return 0.0 } }
fun main() { println(calculateFromString("10", "2")) // Should print: 5.0 println(calculateFromString("abc", "2")) // Should print: Error message println(calculateFromString("10", "0")) // Should print: Error message } ```
Drill 3: Custom Exceptions and Finally Block ​Create a bank account system with custom exceptions.
```kotlin class InsufficientFundsException(message: String) : Exception(message) class InvalidAmountException(message: String) : Exception(message)
class BankAccount(private var balance: Double) { fun withdraw(amount: Double) { try { if (amount <= 0) { throw InvalidAmountException("Withdrawal amount must be positive") } if (amount > balance) { throw InsufficientFundsException("Insufficient funds: Available balance is $balance") } balance -= amount println("Withdrew $amount. New balance: $balance") } catch (e: InsufficientFundsException) { println("Error: ${e.message}") } catch (e: InvalidAmountException) { println("Error: ${e.message}") } finally { println("Transaction completed. Current balance: $balance") } } }
fun main() { val account = BankAccount(1000.0) account.withdraw(500.0) // Should succeed account.withdraw(-50.0) // Should fail with InvalidAmountException account.withdraw(1000.0) // Should fail with InsufficientFundsException } ```
Drill 4: Exception Handling with Collections
Create a function that safely processes a list of mixed data.
This error occurs because the `processItems` function is expecting a parameter of type `List<Any>` but the compiler is inferring a more specific type for the list elements.
Here's the corrected version of your code:
```kotlin data class ProcessingResult( val successCount: Int, val failureCount: Int, val processedItems: List<String> )
fun processItems(items: List<*>): ProcessingResult { val processedItems = mutableListOf<String>() var successCount = 0 var failureCount = 0 items.forEach { item -> try { when (item) { is String -> { processedItems.add(item.uppercase()) successCount++ } is Int -> { processedItems.add(item.toString().padStart(5, '0')) successCount++ } is Double -> { processedItems.add(String.format("%.2f", item)) successCount++ } else -> throw IllegalArgumentException("Unsupported type: ${item?.javaClass?.simpleName}") } } catch (e: Exception) { failureCount++ processedItems.add("ERROR: ${e.message}") } } return ProcessingResult(successCount, failureCount, processedItems) }
fun main() { // Test with mixed types val mixedItems = listOf("hello", 42, 3.14, true, "world", null) val result = processItems(mixedItems) println("Processed ${result.successCount} items successfully") println("Failed to process ${result.failureCount} items") println("Results: ${result.processedItems}") }
megaphone

Processed 4 items successfully Failed to process 2 items Results: [HELLO, 00042, 3.14, ERROR: Unsupported type: Boolean, WORLD, ERROR: Unsupported type: null]

Process finished with exit code 0
Key changes made: 1. Changed `List<Any>` to `List<*>` in the function parameter - this is a star projection that allows for any type of list while maintaining type safety 2. Improved the error message to use the actual class name of the unsupported type 3. Made the code more null-safe by using the safe call operator (?.) when getting the class name
The error occurred because `List<Any>` is too specific and doesn't match the type inference the compiler was making for the list being passed. Using `List<*>` is the idiomatic way in Kotlin to handle lists of mixed types when you don't know all possible types at compile time.
This version will now compile and run correctly, processing different types of items while maintaining type safety.


Drill 5: Network Operations Simulation Create a system that simulates network operations with timeouts and retries.
```kotlin class NetworkException(message: String) : Exception(message) class TimeoutException(message: String) : Exception(message)
class NetworkService { private var failureCount = 0 fun fetchData(url: String, maxRetries: Int = 3): String { var attempts = 0 while (attempts < maxRetries) { try { attempts++ simulateNetworkCall(url) println("Successfully fetched data from $url") return "Data from $url" } catch (e: TimeoutException) { println("Attempt $attempts failed: ${e.message}") if (attempts == maxRetries) { throw NetworkException("Failed after $maxRetries attempts: ${e.message}") } // Exponential backoff Thread.sleep(100L * attempts * attempts) } catch (e: NetworkException) { println("Network error: ${e.message}") throw e } finally { println("Attempt $attempts completed") } } throw NetworkException("Unexpected error occurred") } private fun simulateNetworkCall(url: String) { // Simulate network failures failureCount++ if (failureCount % 2 == 0) { throw TimeoutException("Connection timed out") } if (url.isEmpty()) { throw NetworkException("Invalid URL") } } }
fun main() { val service = NetworkService() try { // Test successful case println("\nTest 1: Successful case") service.fetchData("https://api.example.com/data") // Test retry mechanism println("\nTest 2: Retry mechanism") service.fetchData("https://api.example.com/slow-data") // Test failure case println("\nTest 3: Failure case") service.fetchData("") } catch (e: NetworkException) { println("Final error: ${e.message}") } }
megaphone

Test 1: Successful case Successfully fetched data from https://api.example.com/data Attempt 1 completed

Test 2: Retry mechanism Attempt 1 failed: Connection timed out Attempt 1 completed Successfully fetched data from https://api.example.com/slow-data Attempt 2 completed
Test 3: Failure case Attempt 1 failed: Connection timed out Attempt 1 completed Network error: Invalid URL Attempt 2 completed Final error: Invalid URL
Process finished with exit code 0


Practice Exercises

1. **Error Logger**

Create a system that logs different types of exceptions to different destinations (console, file, network).
2. **Input Validator** Build a form input validator that handles different types of validation errors with custom exceptions.
3. **Resource Manager** Implement a resource management system that ensures resources are properly closed using try-with-resources.
4. **API Response Handler** Create a system that handles different types of API response errors and converts them to appropriate exceptions.
5. **Transaction System** Build a transaction system that rolls back changes if an exception occurs during processing.
### Key Concepts to Remember
1. **Exception Hierarchy** - All exceptions inherit from `Throwable` - `Exception` is the base class for most exceptions - `Error` represents serious system problems
2. **Best Practices** - Catch specific exceptions before general ones - Always provide meaningful error messages - Use finally blocks for cleanup - Don't catch exceptions you can't handle - Document exceptions in function comments
3. **Exception Handling Strategies** - Catch and handle - Catch and rethrow - Let it propagate - Convert to different exception type
4. **Common Use Cases** - Input validation - Resource management - Network operations - File operations - Data processing

Challenge Exercise
Create a comprehensive error handling system for an e-commerce application that: 1. Handles invalid user input 2. Manages inventory errors 3. Processes payment exceptions 4. Handles shipping calculation errors 5. Manages database operation failures
Submit your solution demonstrating proper exception handling for all these scenarios.

info

Custom Exceptions: Comprehensive example using a Smart Home Automation System to demonstrate custom exceptions and architectural thinking.


Custom Exceptions in Smart Home Automation: Architectural Patterns

This example demonstrates how custom exceptions can:
Express domain concepts clearly
Support systematic error handling
Enable automated recovery
Provide rich context for debugging
Support system monitoring and maintenance

Ways to extend this Code Base:
Add more device types and exceptions
Expand the error handling strategies
Add more architectural patterns
Include testing strategies for exceptions

Custom Exceptions in Smart Home Automation


Architectural Design and Implementation Guide
### Introduction
Custom exceptions are more than just error handlers -
They are architectural elements that express domain semantics and system boundaries.

When designing a system, exceptions help define:

- Business rules and constraints
- System boundaries and limitations
- Error recovery paths
- Domain-specific error conditions

Smart Home System Example
Let's model a smart home system where we can identify clear domain boundaries and potential failure points.
```kotlin // Base exception for all smart home related errors sealed class SmartHomeException( message: String, cause: Throwable? = null ) : Exception(message, cause) { // Metadata common to all smart home exceptions abstract val deviceId: String abstract val severity: ErrorSeverity abstract val recoveryAction: RecoveryAction }
// Severity levels for exceptions enum class ErrorSeverity { LOW, // Non-critical, system can continue MEDIUM, // Degraded functionality HIGH, // Critical functionality affected CRITICAL // System safety at risk }
// Possible recovery actions sealed class RecoveryAction { object AutoRecover : RecoveryAction() data class RequiresMaintenance(val serviceCode: String) : RecoveryAction() data class UserAction(val instructions: String) : RecoveryAction() object SystemShutdown : RecoveryAction() }
// Device-specific exceptions class DeviceOfflineException( override val deviceId: String, val lastSeenTimestamp: Long, val attemptedAction: String ) : SmartHomeException( "Device $deviceId is offline. Last seen: $lastSeenTimestamp", null ) { override val severity = ErrorSeverity.MEDIUM override val recoveryAction = RecoveryAction.AutoRecover }
class DeviceOverloadException( override val deviceId: String, val currentLoad: Double, val maxLoad: Double ) : SmartHomeException( "Device $deviceId is overloaded: $currentLoad/$maxLoad", null ) { override val severity = ErrorSeverity.HIGH override val recoveryAction = RecoveryAction.RequiresMaintenance("OL${deviceId}") }
// Security-related exceptions class UnauthorizedAccessException( override val deviceId: String, val attemptedBy: String, val requiredRole: String ) : SmartHomeException( "Unauthorized access to $deviceId by $attemptedBy", null ) { override val severity = ErrorSeverity.CRITICAL override val recoveryAction = RecoveryAction.SystemShutdown }
// Safety-related exceptions class UnsafeOperationException( override val deviceId: String, val operation: String, val unsafeCondition: String ) : SmartHomeException( "Unsafe operation '$operation' attempted on $deviceId: $unsafeCondition", null ) { override val severity = ErrorSeverity.CRITICAL override val recoveryAction = RecoveryAction.UserAction( "Please check device manual for safety instructions" ) }
// Smart Device interface interface SmartDevice { val id: String val type: String val isOnline: Boolean val currentLoad: Double val maxLoad: Double fun performAction(action: String, user: User) }
// Smart Device implementation class SmartLight( override val id: String, private var powerState: Boolean = false ) : SmartDevice { override val type = "LIGHT" override var isOnline = true override var currentLoad = 0.0 override val maxLoad = 100.0 fun turnOn(user: User) { performAction("TURN_ON", user) } override fun performAction(action: String, user: User) { // Check device status if (!isOnline) { throw DeviceOfflineException( deviceId = id, lastSeenTimestamp = System.currentTimeMillis(), attemptedAction = action ) } // Check authorization if (!user.hasPermission("DEVICE_CONTROL")) { throw UnauthorizedAccessException( deviceId = id, attemptedBy = user.id, requiredRole = "DEVICE_CONTROL" ) } // Check load if (currentLoad >= maxLoad) { throw DeviceOverloadException( deviceId = id, currentLoad = currentLoad, maxLoad = maxLoad ) } // Perform action when (action) { "TURN_ON" -> powerState = true "TURN_OFF" -> powerState = false else -> throw IllegalArgumentException("Unknown action: $action") } } }
// Smart Home Controller class SmartHomeController { private val devices = mutableMapOf<String, SmartDevice>() private val errorHandler = SmartHomeErrorHandler() fun addDevice(device: SmartDevice) { devices[device.id] = device } fun executeCommand(deviceId: String, action: String, user: User) { try { val device = devices[deviceId] ?: throw IllegalArgumentException("Device not found: $deviceId") device.performAction(action, user) } catch (e: SmartHomeException) { errorHandler.handleException(e) } } }
// Error Handler class SmartHomeErrorHandler { fun handleException(exception: SmartHomeException) { val errorLog = createErrorLog(exception) when (exception.severity) { ErrorSeverity.CRITICAL -> handleCriticalError(exception, errorLog) ErrorSeverity.HIGH -> handleHighSeverityError(exception, errorLog) ErrorSeverity.MEDIUM -> handleMediumSeverityError(exception, errorLog) ErrorSeverity.LOW -> handleLowSeverityError(exception, errorLog) } when (exception.recoveryAction) { is RecoveryAction.AutoRecover -> attemptAutoRecovery(exception) is RecoveryAction.RequiresMaintenance -> scheduleMaintenance(exception) is RecoveryAction.UserAction -> notifyUser(exception) RecoveryAction.SystemShutdown -> initiateSystemShutdown(exception) } } private fun createErrorLog(exception: SmartHomeException) = """ Error: ${exception.message} Device: ${exception.deviceId} Severity: ${exception.severity} Recovery Action: ${exception.recoveryAction} Timestamp: ${System.currentTimeMillis()} """.trimIndent() private fun handleCriticalError(exception: SmartHomeException, log: String) { println("CRITICAL ERROR: $log") // Notify emergency contacts // Log to secure storage // Trigger alarms if necessary } private fun handleHighSeverityError(exception: SmartHomeException, log: String) { println("HIGH SEVERITY ERROR: $log") // Notify system administrator // Log for immediate review } private fun handleMediumSeverityError(exception: SmartHomeException, log: String) { println("MEDIUM SEVERITY ERROR: $log") // Log for routine review } private fun handleLowSeverityError(exception: SmartHomeException, log: String) { println("LOW SEVERITY ERROR: $log") // Log for statistical analysis } private fun attemptAutoRecovery(exception: SmartHomeException) { println("Attempting auto recovery for device ${exception.deviceId}") // Implement recovery logic } private fun scheduleMaintenance(exception: SmartHomeException) { println("Scheduling maintenance for device ${exception.deviceId}") // Contact maintenance service } private fun notifyUser(exception: SmartHomeException) { println("Notifying user about device ${exception.deviceId}") // Send user notification } private fun initiateSystemShutdown(exception: SmartHomeException) { println("Initiating system shutdown due to critical error on device ${exception.deviceId}") // Implement safe shutdown procedure } }
// Simple User class for demonstration data class User( val id: String, private val permissions: Set<String> ) { fun hasPermission(permission: String): Boolean = permissions.contains(permission) }
fun main() { // Create a smart home controller val controller = SmartHomeController() // Create some devices val light = SmartLight("LIGHT_001") controller.addDevice(light) // Create users with different permissions val adminUser = User("ADMIN_001", setOf("DEVICE_CONTROL")) val regularUser = User("USER_001", setOf()) println("Smart Home System Demo") println("=====================") // Test various scenarios try { println("\nScenario 1: Admin turning on light") controller.executeCommand("LIGHT_001", "TURN_ON", adminUser) println("\nScenario 2: Regular user attempting to turn on light") controller.executeCommand("LIGHT_001", "TURN_ON", regularUser) } catch (e: Exception) { println("Unexpected error: ${e.message}") } // Simulate device going offline light.isOnline = false try { println("\nScenario 3: Attempting to control offline device") controller.executeCommand("LIGHT_001", "TURN_ON", adminUser) } catch (e: Exception) { println("Unexpected error: ${e.message}") } } ```
### Architectural Considerations
1. **Exception Hierarchy** SmartHomeException (sealed) ├── DeviceOfflineException ├── DeviceOverloadException ├── UnauthorizedAccessException └── UnsafeOperationException ```
2. **UML Error Domain Model** ```plantuml @startuml abstract class SmartHomeException { +deviceId: String +severity: ErrorSeverity +recoveryAction: RecoveryAction }
enum ErrorSeverity { LOW MEDIUM HIGH CRITICAL }
abstract class RecoveryAction { +AutoRecover +RequiresMaintenance +UserAction +SystemShutdown }
class DeviceOfflineException { +lastSeenTimestamp: Long +attemptedAction: String }
class DeviceOverloadException { +currentLoad: Double +maxLoad: Double }
SmartHomeException <|-- DeviceOfflineException SmartHomeException <|-- DeviceOverloadException @enduml ```
### Best Practices for Custom Exceptions
1. **Semantic Meaning** - Exceptions should represent meaningful business or system conditions - Names should clearly indicate what went wrong - Include relevant context in exception properties
2. **Hierarchy Design** - Use sealed classes for controlled exception hierarchies - Group related exceptions under common base classes - Consider recovery patterns when designing hierarchy
3. **Context and Recovery** - Include enough information to understand the error - Provide guidance for recovery - Consider adding severity levels - Include device/component identifiers
4. **Documentation** - Document when and why exceptions are thrown - Include example recovery scenarios - Document any automatic retry or recovery mechanisms
### When to Create Custom Exceptions
1. **Domain-Specific Errors** - When standard exceptions don't express the business meaning - For domain-specific validation failures - When different error handling is needed for different conditions
2. **System Boundaries** - At API boundaries - Between major system components - When integrating with external systems
3. **Recovery Patterns** - When different errors require different recovery strategies - For automated recovery systems - When errors need to be handled differently at different levels
4. **Audit and Monitoring** - When errors need specific tracking - For security-related issues - When compliance requirements exist

### Challenge Exercise
Extend the smart home system to include: 1. Temperature control exceptions 2. Network-related exceptions 3. Power management exceptions 4. Integration with a monitoring system
Consider: - Exception hierarchies - Recovery strategies - Logging requirements - User notification patterns
Post your solution to the class Slack Channel demonstrating proper exception handling architecture.
Want to print your doc?
This is not the way.
Try clicking the ⋯ next to your doc name or using a keyboard shortcut (
CtrlP
) instead.