skip to content

Show how to use the value returned by assertFailsWith to assert on an exception's message and its cause.

level: middleimportance: must knowfreq 60%

answer

  1. val ex = assertFailsWith<T> { ... }
  2. ex.message is String? — guard nullability
  3. ex.cause is Throwable? — set when wrapping
  4. assertIs<T> smart-casts the cause
  5. assertContains for substring matches

basics

~10 s

Capture the returned exception in a variable. Then check its message with assertEquals or assertContains, and check its cause by reading ex.cause, asserting its type and message too.

solid answer

~30 s

`assertFailsWith` returns the caught exception, so bind it: `val ex = assertFailsWith<ServiceException> { svc.call() }`. Assert the message with `assertEquals("...", ex.message)` or, for partial matches, `assertContains(ex.message!!, "token")` / `assertTrue(ex.message!!.contains(...))`. To verify the wrapped root cause — set when you rethrow with `throw Wrapper("msg", original)` — read `ex.cause`. Pin its type and message: `val cause = ex.cause; assertIs<IOException>(cause); assertEquals("disk full", cause.message)`. `assertIs<T>` smart-casts so subsequent property access is type-safe. Because `message` is nullable (`String?`), use `assertNotNull(ex.message)` or `!!` deliberately. This captures both the surface contract (message text) and the causal chain (cause), which matters when code wraps low-level exceptions into domain ones.

code

kotlin · 17 lines
kotlin
import kotlin.test.*

class WrapTest {
    class DomainError(msg: String, cause: Throwable) : Exception(msg, cause)

    private fun run(): Nothing =
        throw DomainError("operation failed", IllegalStateException("bad state"))

    @Test
    fun checksMessageAndCause() {
        val ex = assertFailsWith<DomainError> { run() }
        assertEquals("operation failed", ex.message)
        val cause = ex.cause
        assertIs<IllegalStateException>(cause)
        assertEquals("bad state", cause.message)
    }
}

go deeper

for a junior

Can capture the exception and check its message with assertEquals.

for a middle

Handles message nullability and asserts the cause's type and message, using assertIs for smart-casting.

for a senior

Tests the full causal chain to lock down exception-wrapping behavior and explains why assertIs beats assertTrue+is.

for a principal

Defines conventions for asserting failure contracts (message + cause) and avoids brittle exact-string coupling where appropriate.

## Capture, then assert `assertFailsWith<T>` returns the exception, enabling fluent follow-up assertions instead of try/catch. ```kotlin import kotlin.test.* class LoaderTest { @Test fun wrapsLowLevelFailure() { val ex = assertFailsWith<ConfigException> { loadConfig("missing.yml") } // 1) message of the top exception assertEquals("failed to load config", ex.message) // 2) the cause (the wrapped original) val cause = ex.cause assertIs<java.io.FileNotFoundException>(cause) // smart-casts cause assertContains(cause.message ?: "", "missing.yml") } } ``` ## Key APIs - **`ex.message`** — type `String?` (nullable). Guard with `assertNotNull(...)`, `!!`, or `?: ""` before string ops. - **`ex.cause`** — type `Throwable?`. Populated when code does `throw Domain("...", original)` or `initCause(...)`. - **`assertEquals` / `assertContains`** — exact vs substring checks on the message. - **`assertIs<T>(value)`** — asserts `value is T` AND **smart-casts** it, so after the call `cause` is typed as `T` and you can read its members without a manual cast. This is preferable to `assertTrue(cause is T)`, which does not smart-cast a `val` of platform/nullable origin as cleanly. ## Where `cause` comes from The `cause` is part of `Throwable`. Wrapping code sets it via the secondary constructor: ```kotlin try { rawLoad(path) } catch (e: IOException) { throw ConfigException("failed to load config", e) } ``` Here `e` becomes `cause`. Asserting on it verifies the wrapping behavior, not just the surface message. ## Common pitfalls - Forgetting `message` is nullable and calling `.contains` on `String?` (won't compile without `!!`/`?:`). - Asserting the wrapper's message but never checking `cause`, so wrapping regressions slip through. - Using `assertTrue(ex is X)` and then needing a redundant cast — prefer `assertIs<X>`. ## Summary Bind the returned exception → assert `message` (handle nullability) → assert `cause` type and message with `assertIs` to capture the full causal contract.

  • Why prefer assertIs<T>(cause) over assertTrue(cause is T)?
    assertIs<T> both asserts the type and smart-casts the value to T, so afterward you can access T-specific members without a manual cast; it also produces a clearer failure message.
  • Why must you handle ex.message carefully?
    message is typed String?, so calling String operations directly won't compile; you need assertNotNull, !!, or an elvis fallback first.

saying these in an interview costs you the question

  • Calling .contains directly on ex.message without handling null
  • Never asserting cause, so wrapping regressions go unnoticed
  • Using try/catch to grab the exception when the return value already provides it
  • Assuming cause is always non-null

context