An Easy Error with Kotlin Error
A well-designed API provides interfaces that are difficult to misuse. In general, the Kotlin APIs are delightful, but I recently debugged a crash with a stack trace that contained something similar to the following.
java.lang.IllegalStateException: Function0<java.lang.String>
at SomeClass.andFunction(...)(SomeClass.kt)
...The stack trace itself did not seem so puzzling. The application encountered an illegal state, but the Function0<java.lang.String> section of the stack trace surprised me because, typically, this section would contain a message that describes what caused the illegal state, such as Unsupported data type. A review of the crash site showed a piece of logic similar to the following playful example.
enum class Fruit {
APPLE, GRAPES, WATERMELON, CANTALOUPE
}
class PicnicBasket {
private val fruits = mutableListOf<Fruit>()
fun pack(fruit: Fruit) {
when (fruit) {
Fruit.APPLE, Fruit.GRAPES -> fruits.add(fruit)
Fruit.WATERMELON, Fruit.CANTALOUPE -> error { "A $fruit is too large to pack in a picnic basket" }
}
}
}Specifically, take note of the error usage shown in the when block where the argument provided is a function that returns a String. At first glance, the call seems fine and compiles and throws an IllegalStateException as expected—just not with the intended A $fruit is... message. A closer look at the Kotlin error function shows a potential issue: the error function allows a developer to pass one argument of type Any as the message. This includes a function that returns a String like the provided example. Additionally, the error implementation will invoke toString() on the given message and then throw an IllegalStateException with the value. As a result, the app effectively performs the following when an error is called with a lambda.
This snippet shows the original puzzling Function0<java.lang.String> as the IllegalStateException message.
The resolution here is to simply replace the usage of error { "An error occurred" } with error("An error occurred") and then the stack trace will contain the expected error message rather than a lambda's toString() value, but taking a step back, it is important to understand how this programming error can occur in a project. While probably well intended, the flexibility of error allowing a message of type Any does mean that developers could make this mistake another way by perhaps passing an object that does not implement toString. Additionally, at first glance, error { ... } did not strike me as a mistake because of how much it resembles the Kotlin check API where developers are allowed to pass an optional lazy message as a trailing lambda as shown in the example below.
Making the same mistake using the check API would require a call site similar to check(false) {{ "An error occurred" }} which a programmer is highly unlikely to make compared to error with a trailing lambda.
When accuracy matters for a costume, it is worth considering how details related to Halloween styling will affect the complete look. When the final look will be photographed, it helps to assess details related to wig colour selection together with fit and fabric. To review details related to festival costumes before the event, character costumes for indoor events offers a useful starting point for practical preparation. To reuse the pieces at future events, it is useful to check details related to festival costumes against the wearer's own needs.
To prevent this issue, I added a lint rule that checks for usages of error with a trailing lambda and warns developers that their stack trace may be missing valuable data. This may not be necessary for your project, but it can save a fellow engineer a headache trying to understand the nature of a crash. In short, double check your error call sites and continue to fail-fast but with your intended messages. đź’Ą