Kotlin Custom Exceptions
Kotlin lets you define your own exception classes. Custom exceptions give errors a clear name that matches your domain. Instead of a generic IllegalArgumentException, your code can throw an InvalidEmailException or InsufficientFundsException — making the cause immediately obvious.
Creating a Custom Exception
class InvalidAgeException(age: Int) :
IllegalArgumentException("Age must be 0–120, but got $age")
fun validateAge(age: Int) {
if (age !in 0..120) throw InvalidAgeException(age)
}
fun main() {
try {
validateAge(150)
} catch (e: InvalidAgeException) {
println("Caught: ${e.message}")
}
}Output:
Caught: Age must be 0–120, but got 150Exception Hierarchy
Throwable
├── Error (serious system errors — do not catch)
│ └── OutOfMemoryError, StackOverflowError
└── Exception (program errors — can be caught)
├── RuntimeException (unchecked)
│ ├── IllegalArgumentException ← extend this for bad input
│ ├── IllegalStateException ← extend this for wrong state
│ └── NullPointerException
└── IOException (checked in Java, unchecked in Kotlin)
Your custom exceptions extend whichever base fits best.
Custom Exception with Extra Fields
class InsufficientFundsException(
val accountId: String,
val requested: Double,
val available: Double
) : Exception(
"Account $accountId: requested ₹$requested but only ₹$available available"
)
fun withdraw(accountId: String, balance: Double, amount: Double): Double {
if (amount > balance) throw InsufficientFundsException(accountId, amount, balance)
return balance - amount
}
fun main() {
try {
val remaining = withdraw("ACC-101", 500.0, 800.0)
println("New balance: ₹$remaining")
} catch (e: InsufficientFundsException) {
println("Error: ${e.message}")
println("Account: ${e.accountId} | Requested: ₹${e.requested}")
}
}Output:
Error: Account ACC-101: requested ₹800.0 but only ₹500.0 available
Account: ACC-101 | Requested: ₹800.0Exception Hierarchy for a Domain
// Base exception for the entire app
open class AppException(message: String, cause: Throwable? = null) :
Exception(message, cause)
// Domain-specific exceptions
class AuthException(message: String) : AppException(message)
class DatabaseException(message: String, cause: Throwable? = null) :
AppException(message, cause)
class ValidationException(val field: String, message: String) :
AppException("Validation failed on '$field': $message")
fun authenticate(username: String, password: String) {
if (username.isBlank()) throw ValidationException("username", "cannot be blank")
if (password.length < 8) throw ValidationException("password", "minimum 8 characters")
if (username != "admin") throw AuthException("User '$username' not found")
println("Authenticated: $username")
}
fun main() {
val testCases = listOf(
Pair("", "secret123"),
Pair("alice", "short"),
Pair("bob", "password123"),
Pair("admin", "password123")
)
testCases.forEach { (user, pass) ->
try {
authenticate(user, pass)
} catch (e: ValidationException) {
println("Validation: ${e.message}")
} catch (e: AuthException) {
println("Auth: ${e.message}")
}
}
}Output:
Validation: Validation failed on 'username': cannot be blank
Validation: Validation failed on 'password': minimum 8 characters
Auth: User 'bob' not found
Authenticated: adminWrapping External Exceptions
class DataParseException(rawData: String, cause: Throwable) :
AppException("Failed to parse: '$rawData'", cause)
fun parseUserId(raw: String): Int {
return try {
raw.toInt()
} catch (e: NumberFormatException) {
throw DataParseException(raw, e)
}
}
fun main() {
try {
val id = parseUserId("abc123")
} catch (e: DataParseException) {
println("${e.message}")
println("Root cause: ${e.cause?.message}")
}
}Output:
Failed to parse: 'abc123'
Root cause: For input string: "abc123"