skip to content

Kotlin contracts are unverified by the compiler. Explain the soundness risk of returns effects, when smart casts driven by a contract can silently break, and how this constrains where you should declare them.

level: seniorimportance: should knowfreq 28%

answer

  1. Compiler trusts, never verifies the body
  2. Bad contract → ClassCastException at use site
  3. Error appears far from the buggy function
  4. Condition limited to params; cast needs stable target
  5. Risky to expose in published library APIs

basics

~20 s

The compiler believes your contract without checking the body. If the body doesn't actually enforce the promised condition, the compiler smart-casts based on a false promise, which can throw at runtime. So contracts must be written carefully and kept in sync with the body.

solid answer

~50 s

Returns effects are a *trusted* assertion: the compiler extracts the contract and reasons from it, but never proves the function body honors it. If require-style validation is missing or wrong, the compiler still emits an unchecked smart cast, producing a ClassCastException or NullPointerException at the use site rather than at the failing function. This is the core soundness gap. It also breaks if the implies condition references something that can change — but contracts restrict conditions to parameters/receiver, and the smart cast at the call site still needs a *stable* target (val or unmodified local var). Practical constraints: keep the contract adjacent to and consistent with the validation logic; avoid contracts on functions whose bodies might be overridden or change; do not expose contract guarantees you cannot maintain across versions, because callers compile against the promise. Prefer mirroring the proven stdlib patterns (require/check/isNullOrEmpty).

go deeper

for a junior

Knows contracts are experimental and should be written carefully.

for a middle

Explains that the compiler trusts the contract and a wrong one can crash at runtime.

for a senior

Pinpoints that failures surface at the use site, ties in stability rules, and advises keeping contract and body in sync.

for a principal

Reasons about library API/compatibility risk of exposing contracts and why Kotlin chose an unverified trust model.

## The trust model Kotlin contracts are **declarative and unverified**. When you write: ```kotlin contract { returns() implies (x is String) } ``` the compiler records "surviving this call ⇒ x is String" and uses it to smart-cast at every call site. It does **not** analyze the body to confirm that the function really throws when `x` is not a `String`. The contract is taken on faith. ### Where it silently breaks 1. **Body doesn't enforce the promise.** ```kotlin @OptIn(ExperimentalContracts::class) fun assertString(x: Any?) { contract { returns() implies (x is String) } // BUG: forgot the actual check } fun go(v: Any?) { assertString(v) println(v.length) // compiles; throws ClassCastException at runtime if v isn't String } ``` The error surfaces far from the buggy function — at the **use site**, as a `ClassCastException`/`NullPointerException`, making it hard to trace. 2. **Contract drifts from body over time.** A later refactor weakens validation but leaves the contract; every caller keeps the now-false smart cast. 3. **Wrong return mode.** Using `returns()` where the function actually returns a Boolean (and callers should branch) gives an unconditional guarantee that doesn't hold on the false path. ## What contracts canNOT break (by design) - The implies condition is restricted to `is`/`!is`/null checks on **parameters/receiver**, so you cannot promise things about arbitrary mutable state. - The smart cast at the call site still obeys normal **stability** rules: only a `val` or an unmodified local `var` is narrowed. A captured var, a `var` with custom getter, or a property of another module won't smart-cast even with a correct contract. ## How this constrains usage - **Keep the contract and its enforcing code together and reviewed as a unit.** Treat changing one as changing the other. - **Prefer throwing helpers** (`returns() implies`) where the body unconditionally validates, or Boolean predicates with `returns(true/false)` where branching is natural — match the mode to reality. - **Be cautious exposing contracts in a published library API:** callers compile against the promise; if a later version stops honoring it you have a binary/behavioral compatibility break that manifests as crashes in client code. - **Avoid clever conditions you can't guarantee forever**; the safest contracts mirror the stdlib's (`require`, `check`, `requireNotNull`, `isNullOrEmpty`). ## Why Kotlin accepts the risk Verifying arbitrary bodies against contracts is undecidable in general and expensive. Kotlin chose an experimental, opt-in, trust-based design to unlock the ergonomics (no redundant `!!`/casts) while keeping the compiler simple — hence the `ExperimentalContracts` marker.

  • Where does the runtime failure of a wrong returns() implies (x is String) contract actually appear?
    At the caller's use of x (e.g. x.length), as a ClassCastException, not inside the function with the bad contract — which makes debugging harder.
  • Does a correct contract guarantee the smart cast always happens?
    No. The call-site target must still be stable: a val or an unmodified, uncaptured local var. A mutable/captured target won't smart-cast even with a perfect contract.

A contract is a notarized claim the compiler never fact-checks: if you lie on the form, no one stops you at signing — the lie blows up later, somewhere else.

saying these in an interview costs you the question

  • Believing the compiler proves the body satisfies the contract
  • Assuming a contract makes any variable smart-castable regardless of stability
  • Treating contracts as zero-risk because 'they're stdlib-blessed'
  • Not realizing failures surface at the call site, not the function

context