skip to content

How does TODO() differ from error(), check()/require(), and an empty stub body? When would you choose each, and what are the design implications of shipping TODO() to production?

level: seniorimportance: should knowfreq 25%

answer

  1. TODO -> NotImplementedError (Error), 'not built'
  2. error/check -> IllegalStateException, invariant/state
  3. require -> IllegalArgumentException, caller args
  4. Empty body -> silent no-op, dangerous
  5. Guard live TODO with review + Detekt + tests

basics

~20 s

TODO() means 'not built yet' and crashes if hit. error() means 'this should never happen'. require/check validate inputs/state. An empty body silently does nothing. Pick based on intent; never leave a live TODO() for users.

solid answer

~40 s

All of TODO(), error(), check(), and require() can return/throw on a Nothing-style path, but they signal different intents. `TODO(reason)` throws `NotImplementedError` (an Error) and means 'implementation missing' — a development marker. `error(message)` throws `IllegalStateException` and means an invariant was violated / unreachable-state. `check(cond) { msg }` throws `IllegalStateException` for invalid internal state; `require(cond) { msg }` throws `IllegalArgumentException` for invalid caller arguments. An empty `{}` body returns Unit silently — wrong for non-Unit returns and dangerous because it hides missing logic. Choose by intent: stub -> TODO; impossible branch -> error; precondition on args -> require; precondition on state -> check. Shipping a live TODO() means an NotImplementedError reaches users on that path; guard with code review, lint/Detekt rules, and tests that exercise stubbed paths.

code

kotlin · 8 lines
kotlin
fun withdraw(amount: Int) {
    require(amount > 0) { "amount must be positive" }     // IllegalArgumentException
    check(account.isOpen) { "account is closed" }          // IllegalStateException
    when (account.type) {
        Type.CHECKING -> debitChecking(amount)
        Type.CRYPTO   -> TODO("crypto withdrawals pending") // NotImplementedError if reached
    }
}

go deeper

for a junior

Knows TODO() crashes and is a placeholder, vaguely aware of require/check.

for a middle

Maps each construct to its thrown type and basic intent (args vs state vs not-implemented).

for a senior

Chooses correctly by intent, explains why an empty body is dangerous, and proposes guards against shipping live TODO().

for a principal

Defines team policy: lint/Detekt enforcement, branch-coverage requirements, error taxonomy, and how stub markers fit the delivery process.

## The four tools and what they throw | Construct | Throws | Type | Intent | |---|---|---|---| | `TODO(reason)` | `NotImplementedError` | `Error` | Code not implemented yet (dev marker) | | `error(msg)` | `IllegalStateException` | `RuntimeException` | Unreachable / invariant broken | | `check(cond) { msg }` | `IllegalStateException` | `RuntimeException` | Invalid **internal state** | | `require(cond) { msg }` | `IllegalArgumentException` | `RuntimeException` | Invalid **caller arguments** | | empty `{}` body | nothing | — | Silently no-op (returns Unit) | All except the empty body return `Nothing` (or are declared to throw), so they satisfy any expected type and mark the path dead. ```kotlin fun parse(input: String?): Config { require(input != null) { "input must not be null" } // bad argument -> IAE check(state == State.READY) { "parser not ready" } // bad state -> ISE return when (input.first()) { '{' -> parseJson(input) '<' -> TODO("XML parsing not built yet") // missing impl -> NotImplementedError else -> error("unsupported format") // impossible/invalid -> ISE } } ``` ## Why intent matters - Readers and tools interpret `NotImplementedError` as 'someone hasn't finished this', whereas `IllegalStateException` reads as 'a real runtime invariant failed'. Mixing them muddies diagnostics. - `require`/`check` carry standard semantics that callers and libraries (and Kotlin contracts) understand: `require` => bug in the caller, `check`/`error` => bug here. ## The danger of an empty stub body ```kotlin fun applyDiscount() { /* TODO later */ } // silently does nothing ``` This compiles, runs, and produces **wrong behaviour silently** — far worse than `TODO()`, which fails loudly. For a non-Unit function you literally can't leave it empty, which is one reason `TODO()` exists. ## Shipping TODO() to production A live `TODO()` on a reachable path throws `NotImplementedError` for real users. Mitigations: - **Code review / PR checklist** to catch stubs. - **Static analysis**: Detekt's `ForbiddenMethodCall` (configurable) or custom rules can flag `kotlin.TODO`. - **Tests** that exercise every branch will surface a stubbed path immediately. - Treat `TODO()` as a *temporary, must-fail-loud* marker, not a graceful fallback. If you want a graceful default, implement it explicitly. ## Rule of thumb Stub -> `TODO`. Impossible/invariant -> `error`/`check`. Bad argument -> `require`. Never an empty body to 'skip' real logic.

  • What exception type does error() throw versus TODO()?
    error() throws IllegalStateException (a RuntimeException); TODO() throws NotImplementedError (a subclass of Error).
  • Why is leaving a function body empty often worse than TODO()?
    An empty body silently does nothing and returns Unit, hiding the missing logic; TODO() fails loudly so the gap is obvious in testing.
  • How would you prevent a live TODO() from reaching production?
    Branch-covering tests, code review, and a static-analysis rule (e.g., Detekt ForbiddenMethodCall) flagging kotlin.TODO on shipped paths.

TODO() is scaffolding with a 'do not enter' alarm; error()/check() are structural alarms for a finished building; an empty body is a missing floor with no warning sign.

saying these in an interview costs you the question

  • Saying TODO() and error() are interchangeable
  • Using require() for internal-state checks (it's for arguments)
  • Recommending an empty body as a safe placeholder
  • Treating TODO() as a graceful fallback for users
  • Claiming all four throw the same exception type

context