The Case of the Missing Handler

Matt McKennaMatt McKenna
4 min read

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!

Here’s a Kotlin Playground where I set up a 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!

0
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