The Case of the Missing Handler


Our story begins with a clean line of Kotlin:
typealias Handler = (result: Result) -> Unit
It looks innocent. Give a function type a name and tidy up the signatures, great!
Then a bug hits. The Handler is gone in the debugger and in stack traces only (Result) -> Unit
remains. Our alias has disappeared at the scene of the crime.
That is the trick, a typealias
does not create a new type. It is only a name for the function type.
Enter the functional interface
fun interface Handler {
fun handle(result: Result)
}
Now the Handler exists. It has identity, it can carry documentation, and the method name handle
shows up in stack traces and search results. Usage is still light:
val handler: Handler = Handler { result ->
println(result)
}
Evidence
Overloading
Aliases collapse to the same function type, which prevents overloading:
typealias Handler = (Result) -> Unit
typealias Completion = (Result) -> Unit
fun process(h: Handler) { }
fun process(c: Completion) { } // Won't compile, same JVM signature.
Functional interfaces are distinct types, so overloads compile:
fun interface Handler {
fun handle(result: Result)
}
fun interface Completion {
fun complete(result: Result)
}
fun process(h: Handler) { }
fun process(c: Completion) { }
Multiple handlers with the same alias
Using a single alias for several callbacks in the same type invites mistakes. The compiler cannot help if you swap them. It also won’t tell you which was called while debugging or looking at a stacktrace.
// One alias for two different callbacks
typealias Handler = (Result) -> Unit
data class Screen(
val onClick: Handler,
val onDismiss: Handler
)
val click: Handler = { println("clicked: $it") }
val dismiss: Handler = { println("dismissed: $it") }
// Compiles, but the handlers are reversed
val screen = Screen(onClick = dismiss, onDismiss = click)
With functional interfaces you can give each callback a distinct type, so it is impossible to construct the Screen
incorrectly and stacktraces name the method you expect.
fun interface ClickHandler { fun onClick(result: Result) }
fun interface DismissHandler { fun onDismiss(result: Result) }
data class Screen(
val onClick: ClickHandler,
val onDismiss: DismissHandler
)
// Will not compile if you swap types
// val screen = Screen(onClick = DismissHandler { ... }, onDismiss = ClickHandler { ... })
val screen = Screen(
onClick = ClickHandler { println("clicked: $it") },
onDismiss = DismissHandler { println("dismissed: $it") }
)
Kdoc and Discoverability
You can put kdoc on a functional interface and it surfaces in the IDE. You can also add small helpers as extensions:
/** Called once per operation with the final result. */
fun interface Handler {
fun handle(result: Result)
}
fun Handler.logged(): Handler = Handler { result ->
Log.d("Handler", "Handled: $result")
this.handle(result)
}
Stacktrace Comparison
With a typealias
, the alias name is not present. You will usually see $lambda$
or Function1
:
java.lang.IllegalStateException: boom in typealias
at FileKt.main$lambda$0(File.kt:27) // What was called here??
at FileKt.main(File.kt:30)
at FileKt.main(File.kt)
With a functional interface, the interface and its method are visible, which makes searching and triage simpler:
java.lang.IllegalStateException: boom in fun interface
at LoggingHandler.handle(File.kt:18) // Clear name of the caller and override!
at FileKt.main(File.kt:38)
at FileKt.main(File.kt)
This will also happen in the call stack in the debugger leaving you stranded as you attempt to navigate through your breakpoints.
Testing with fakes
A named type makes simple fakes straightforward:
val calls = mutableListOf<Result>()
val handler: Handler = Handler { result -> calls += result }
// exercise code that calls handler.handle(...)
check(calls.isNotEmpty())
Verdict
typealias
makes code look cleaner but they do not provide type identity. If you want names that survive code navigation, debugging, and evolution, prefer a functional interface.
Try it yourself!
typealias
and fun interface
to capture the stack trace to see the difference in the output!When to use a typealias
?
When dealing other cases that aren’t lambdas, especially with complicated or nested generic types. They are great for turning complex types into readable, reference-able, and remember-able types!
Subscribe to my newsletter
Read articles from Matt McKenna directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Matt McKenna
Matt McKenna
Android GDE @ Square he/him #BlackLivesMatter #StopAsianHate