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
}
```
#### 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}")
}
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}")
}
}
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.
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}")
}
}
```
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