How do you declare and safely invoke a property that is both nullable and a suspend function, e.g. an optional `suspend () -> Unit` callback?
answer
- `(suspend () -> Unit)?` — parens wrap the suspend type
- Need BOTH: `?.invoke()` and a suspend context
- Parens distinction vs `suspend () -> Unit?`
- var → snapshot to local before smart-cast call
- no-op default lambda avoids the nullability
basics
~10 sDeclare it as (suspend () -> Unit)?. Call it from inside a coroutine with callback?.invoke(), which runs it only if it is not null.
solid answer
~40 sYou combine both type features: wrap the suspend function type in parentheses and add `?`: `val onDone: (suspend () -> Unit)? = null`. The parentheses matter — `?` must apply to the entire `suspend () -> Unit`. Invocation needs both rules satisfied: it must be non-null AND in a suspend context. So you write `onDone?.invoke()` from inside a `suspend` function or coroutine builder. The safe call handles the nullability; the surrounding suspend context handles the suspend requirement. `onDone()` won't compile (nullable), and `onDone?.invoke()` from non-suspend code won't compile either (no coroutine context). For a mutable `var`, copy to a local before a smart-cast call, or just use `?.invoke()`.
code
kotlin · 8 linesclass Downloader(private val onComplete: (suspend () -> Unit)? = null) {
suspend fun download(url: String) {
delay(100) // pretend network
onComplete?.invoke() // null-safe AND in a suspend fun
}
}
// runBlocking { Downloader { delay(10); println("done") }.download("x") }go deeper
Can write (suspend () -> Unit)? and call ?.invoke() from inside a coroutine when shown the pattern.
Independently combines nullability + suspend, explains both compile-time requirements, and the parentheses semantics.
Weighs nullable-callback vs no-op-default design and handles the mutable-var smart-cast pitfall correctly.
Considers cancellation, exception propagation, and structured concurrency when an optional suspend callback runs inside a scope.
## Declaring the combined type You stack two type features on the function type: ```kotlin val onDone: (suspend () -> Unit)? = null ``` - `suspend () -> Unit` is the suspend function type. - Wrapping it in parentheses and adding `?` makes the **whole thing** nullable. - The parentheses are mandatory. `suspend () -> Unit?` (no outer parens) would be a *non-null* suspend function returning a *nullable Unit* — a different type. ## Invoking it: two requirements at once To call the value you must satisfy **both**: 1. **Null-safety** — use a safe call `?.invoke()` (or a null check), because the reference may be null. 2. **Suspend context** — the call site must be a `suspend` function or coroutine builder. ```kotlin class Task(val onDone: (suspend () -> Unit)? = null) { suspend fun run() { // ... do work ... onDone?.invoke() // safe call + we are in a suspend fun } } ``` - `onDone()` → compile error (nullable, call operator not allowed). - `onDone?.invoke()` in a **non-suspend** function → compile error ("suspend function should be called only from a coroutine or another suspend function"). - `onDone?.invoke()` in a **suspend** function → correct. ## Mutable property caveat If it is a `var`, you cannot rely on a smart cast (`if (onDone != null) onDone()`), because a mutable member could change. Either use `?.invoke()` or snapshot it: ```kotlin val cb = onDone if (cb != null) cb() // local val smart-casts fine, still needs suspend context ``` ## Passing args / defaults For `(suspend (Int) -> Unit)?` you call `onDone?.invoke(42)`. To provide a no-op default you can use a non-null suspend lambda `{ }` instead of nullability: `val onDone: suspend () -> Unit = {}` — then no `?.` is needed, only the suspend context.
- Why won't `onComplete?.invoke()` compile inside a non-suspend method even though it handles null?The safe call only fixes nullability. The value is still a suspend function type, so invoking it requires a suspend context; a non-suspend method has none, giving a separate compile error.
- What does `(suspend () -> Unit)?` differ from `suspend () -> Unit?`?The first is a nullable suspend function returning Unit; the second is a non-null suspend function returning a nullable Unit. The outer parentheses decide what `?` binds to.
saying these in an interview costs you the question
- Writing `suspend () -> Unit?` and thinking the function itself is nullable
- Calling it from a non-suspend function and expecting it to work
- Using `onDone()` directly on the nullable type
- Believing `?.invoke()` removes the need for a coroutine context
- Smart-casting a mutable var member without snapshotting