Exceptions should carry actionable context
Use require for invalid arguments and check for invalid internal state. Define a custom exception when callers need to distinguish a meaningful failure category, not merely to rename every built-in exception.
class InvalidLessonId(val supplied: String) : IllegalArgumentException("Invalid lesson ID")
fun validateId(value: String): String {
if (!value.matches(Regex("[a-z][a-z0-9-]*"))) throw InvalidLessonId(value)
return value
}
The exception type allows targeted handling. Do not include tokens or sensitive user input in public messages or logs. When wrapping a lower-level exception, preserve it as the cause so diagnostics keep the original stack trace.
Expected validation in a form may be better represented as a value, avoiding an exception for every mistyped character. The right representation depends on how callers are expected to recover.
Exercise
Add a repository exception with an operation name and cause. Write a test asserting the original cause remains available after wrapping.
Check: catching your exception should not require parsing its human-readable message to determine the failure category.