Kotest's shouldThrow<T> block returns a value. What is that value, how do you assert on it, and when do you need the shouldThrowUnit variant instead?
answer
- shouldThrow returns the exception, typed T
- reified T → typed fields, no cast
- shouldHaveMessage / shouldHaveCauseInstanceOf
- Unit-returning block → shouldThrowUnit
- assertions after the throw inside the block never run
basics
~20 sIt returns the caught exception, already typed as T. Bind it and assert on message, cause or custom fields. Use shouldThrowUnit<T> when the block's last expression is Unit, because plain shouldThrow expects a block returning Any?.
solid answer
~50 s`shouldThrow<T> { ... }` is not a void assertion: it returns the thrown exception typed as `T`, so the natural style is `val ex = shouldThrow<IllegalArgumentException> { service.rename("") }` and then assert on it — `ex.message shouldContain "blank"`, `ex.shouldHaveMessage("name must not be blank")`, `ex.shouldHaveCauseInstanceOf<SQLException>()`, or on your own exception's typed fields (`ex.errorCode shouldBe 409`). Because the return type is the reified `T`, you get those fields without a cast. `shouldThrowExactly<T>` returns `T` as well; `shouldThrowAny` returns `Throwable`, so you only get message/cause unless you narrow it yourself. The `shouldThrowUnit<T>` / `shouldThrowExactlyUnit` / `shouldThrowAnyUnit` variants exist for a Kotlin typing reason: the main overload takes `() -> Any?`, and a block whose last statement is `Unit`-returning can fail to infer cleanly. If the compiler complains about the block type, switch to the `Unit` variant — behaviour is otherwise identical.
code
kotlin · 7 linesval ex = shouldThrow<PaymentDeclinedException> {
payments.charge(card, amount = 500)
}
ex.shouldHaveMessage("card declined: insufficient funds")
ex.declineCode shouldBe "51"
ex.shouldHaveCauseInstanceOf<GatewayTimeoutException>()go deeper
Know that the block returns the caught exception and that you assert on it after the block, not inside it.
Explain the reified return type, asserting on message and cause with the throwable matchers, and why the Unit variants exist.
Argue for naming the narrowest exception type, prefer substring/regex message checks over exact equality, and use cause matchers for wrapped failures.
Set the team convention: exceptions are part of the public contract, so tests assert on type plus a stable machine-readable field (an error code), not on human-facing message text.
## The shape of the assertion In Kotest's assertions library (`io.kotest.assertions.throwables`), the exception assertions are *expressions*, not statements. `shouldThrow<T> { block }` runs the block, and: - if nothing is thrown, it fails with "Expected exception ... but no exception was thrown"; - if something of type `T` (or a subtype) is thrown, it **returns that throwable, typed as `T`**; - if something else is thrown, it fails and reports what was thrown instead. That return value is the whole point of this design. Many frameworks make you pass the assertion into a callback or re-catch the exception yourself; Kotest lets you write the arrange/act inside the lambda and keep the assertions outside it, where they read like every other assertion in the test. ## Using the returned exception ```kotlin val ex = shouldThrow<AccountLockedException> { accounts.login("ada", "wrong") } ex.message shouldContain "locked" ex.remainingAttempts shouldBe 0 ``` Because `T` is reified and the return type is `T`, `remainingAttempts` — a property that exists only on your exception class — is reachable with no cast and no smart-cast dance. This is why over-broad expectations hurt: `shouldThrow<Exception>` compiles but gives you back only `Exception`, so you lose the typed accessors and you also weaken the assertion (any exception subtype passes). Kotest ships throwable matchers in `io.kotest.matchers.throwable` that pair with the returned value: - `shouldHaveMessage("...")` — asserts the full message; - `shouldHaveCause { it.shouldBeInstanceOf<IOException>() }` — asserts a cause exists and lets you assert on it; - `shouldHaveCauseInstanceOf<T>()` (subtype allowed) and `shouldHaveCauseOfType<T>()` (exact type); - `shouldNotHaveCause()`. For partial message checks, prefer a string matcher on `ex.message` (`shouldContain`, `shouldMatch` with a regex) over exact equality: messages built from interpolated values, locales, or third-party libraries change more often than the behaviour you actually care about. ## The Unit variants The primary overload is declared over a block returning `Any?`. When the block's last expression is a `Unit`-returning call, Kotlin's inference can pick an unexpected overload or complain, especially when the block is a single statement. Kotest therefore ships parallel functions whose block type is `() -> Unit`: - `shouldThrowUnit<T> { ... }` - `shouldThrowExactlyUnit<T> { ... }` - `shouldThrowAnyUnit { ... }` Semantics are identical; only the block's declared return type differs. A practical rule: write `shouldThrow` first, and only reach for the `Unit` variant if the compiler objects. A common workaround people use instead — adding a trailing dummy expression to the block — works but is noise. ## The negative forms The family also has `shouldNotThrowAny { ... }`, `shouldNotThrow<T> { ... }` and `shouldNotThrowExactly<T> { ... }`. `shouldNotThrowAny` returns the block's own value, which makes it useful as a wrapper around a call whose result you then assert on — it converts an unexpected exception into a readable assertion failure instead of a raw stack trace, which matters mostly for diagnostics rather than for pass/fail. ## Common mistakes 1. **Discarding the return value** and then re-running the call outside the block to inspect the exception — the call runs twice, and side effects run twice with it. 2. **Catching manually** (`try { ... ; fail("expected") } catch (e: X) { ... }`) — verbose, and it is easy to forget the `fail` so the test passes when nothing throws. 3. **Putting assertions inside the block** after the throwing call. They never execute, so the test silently asserts nothing beyond "something was thrown". 4. **Asserting on `toString()`** of the exception rather than `message`; `toString()` includes the class name and is brittle. 5. **Using `shouldThrowAny` and then casting.** If you know the type, name it and let the return type do the work. ## Why interviewers ask It separates people who have only copied a `shouldThrow { }` line from people who use it as a value-producing assertion. The follow-up is usually about asserting on causes in wrapped exceptions — a real-world case where the returned exception plus `shouldHaveCauseInstanceOf` is the shortest correct answer.
- How would you assert on a wrapped exception, where the interesting failure is the cause?Capture the outer exception from `shouldThrow<T>` and then assert on the cause: `ex.shouldHaveCauseInstanceOf<SQLException>()` allows subtypes, while `shouldHaveCauseOfType<T>()` requires the exact class. `shouldHaveCause { ... }` gives you a block to run further assertions on the cause itself. Avoid asserting on the outer message alone — wrapper messages are usually the least stable part of the chain.
- Why is `shouldThrow<Exception> { ... }` usually a weak assertion?Because it passes for any exception subtype, including a `NullPointerException` from a bug in the arrangement code, so the test can go green for the wrong reason. It also returns only `Exception`, so you lose access to the typed fields of your own exception class and must cast. Always name the narrowest type the contract actually promises.
- Do assertions written after the throwing call inside the block run?No. The block aborts at the throw, so anything after it is dead code and the test asserts far less than it appears to. The correct shape is: only the act inside the block, all assertions on the returned exception outside it.
saying these in an interview costs you the question
- Thinking shouldThrow returns Unit and re-invoking the call to inspect the exception
- Putting the assertions inside the shouldThrow block, after the throwing line
- Using shouldThrow<Exception> or shouldThrowAny and then casting to the real type
- Asserting exact equality on messages produced by third-party libraries or interpolation
- Believing shouldThrowUnit has different assertion semantics rather than just a different block type