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?
answer
- TODO -> NotImplementedError (Error), 'not built'
- error/check -> IllegalStateException, invariant/state
- require -> IllegalArgumentException, caller args
- Empty body -> silent no-op, dangerous
- Guard live TODO with review + Detekt + tests
basics
~20 sTODO() 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 sAll 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 linesfun 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
Knows TODO() crashes and is a placeholder, vaguely aware of require/check.
Maps each construct to its thrown type and basic intent (args vs state vs not-implemented).
Chooses correctly by intent, explains why an empty body is dangerous, and proposes guards against shipping live TODO().
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