LY Corporation Tech Blog

We are promoting the technology and development culture that supports the services of LY Corporation and LY Corporation Group (LINE Plus, LINE Taiwan and LINE Vietnam).

This post is also available in the following languages. Japanese

Improving code quality - Session 32: Return from the abyss

The original article was published on July 4, 2024.

Hello, I'm Munetoshi Ishikawa, a mobile client developer for the LINE messaging app.

This article is the latest installment of our weekly series "Improving code quality". For more information about the Weekly Report, please see the first article.

Return from the abyss

Early return is an effective technique to separate cases where a function achieves its main purpose (happy path) from cases where it does not (unhappy path), making the function flow easier to understand. However, careful consideration is needed on how to apply early return.

In the following code, return is used for unexpected cases of messageData.mainMimeType and failed saving of messageData.content, but it makes the code harder to read. How can it be improved?

fun ...(...) {
    try {
        val messageType = if (messageData.isParsed) {
            messageData.messageType
        } else {
            when (messageData.mainMimeType) {
                "image" -> MessageType.IMAGE
                "video" -> MessageType.VIDEO
                else -> return false
            }
        }

        val saveResult = saveContent(messageData.content, messageType) // IOException may be thrown
        if (!saveResult.isSuccessful) {
            return false
        }
        return true
    } catch (_: IOException) {
        return false
    }
}

Create a clear return path

The return in the previous function is hard to understand for the following two reasons:

  1. There are return statements from deeply nested places or parts of branches (e.g., the first return false is inside try, if, and when, and the conditions to reach there are not immediately clear)
  2. The return does not significantly contribute to separating the happy path from the unhappy path (e.g., the return false for !saveResult.isSuccessful has no subsequent actions)

When using early return, it is important to place return in prominent locations, make conditions clear, and highlight the happy path.

For example, by doing the following, it becomes clear that "early return occurs when messageType becomes null". Also, by looking at the last line, it is easy to understand that true is returned when the message is saved successfully.

fun ...(...) {
    val messageType = messageData.messageType.takeIf { messageData.isParsed }
        ?: MAIN_MIME_TO_TYPE_MAP[messageData.mainMimeType]
        ?: return false

    val saveResult = try {
        saveContent(messageData.content, messageType)
    } catch (_: IOException) {
        SaveResult.Error()
    }
    return saveResult.isSuccessful
}

companion object {
    val MAIN_MIME_TO_TYPE_MAP: Map<String, MessageType> = mapOf(
        "image" to MessageType.IMAGE,
        "video" to MessageType.VIDEO
    )
}

Especially when using multi-branch structures like when or switch, writing code that returns for only some conditions can lead to overlooking the early return itself. For example, in the code converting Foo to Bar below, you might miss the return in the INVALID case.

fun ...(foo: Foo) {
    val bar = when (foo) {
        Foo.FIRST -> Bar.FIRST
        Foo.SECOND -> Bar.SECOND
        Foo.THIRD -> Bar.THIRD
        Foo.INVALID -> return
    }

    ...
}

This issue can be resolved by separating multi-branch structures and early returns. In the improvement proposal below, the code converting Foo to Bar is extracted as a function.

fun ...(foo: Foo) {
    val bar = toBar(foo)
        ?: return

    ...
}

private fun toBar(foo: Foo): Bar? = when (foo) {
    Foo.FIRST -> Bar.FIRST
    Foo.SECOND -> Bar.SECOND
    Foo.THIRD -> Bar.THIRD
    Foo.INVALID -> null
}

This emphasizes that "early return occurs when Foo cannot be converted to Bar".

For more on early return, refer to the "Code Readability" presentation  (slides 39-48).

In a nutshell

When using early return, make its presence, conditions, and the happy path stand out.

Keywords: early return, conditional branch, function flow

List of articles on techniques for improving code quality