skip to content

How do you declare and safely invoke a property that is both nullable and a suspend function, e.g. an optional `suspend () -> Unit` callback?

level: middleimportance: should knowfreq 40%

answer

  1. `(suspend () -> Unit)?` — parens wrap the suspend type
  2. Need BOTH: `?.invoke()` and a suspend context
  3. Parens distinction vs `suspend () -> Unit?`
  4. var → snapshot to local before smart-cast call
  5. no-op default lambda avoids the nullability

basics

~10 s

Declare 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 s

You 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 lines
kotlin
class 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

for a junior

Can write (suspend () -> Unit)? and call ?.invoke() from inside a coroutine when shown the pattern.

for a middle

Independently combines nullability + suspend, explains both compile-time requirements, and the parentheses semantics.

for a senior

Weighs nullable-callback vs no-op-default design and handles the mutable-var smart-cast pitfall correctly.

for a principal

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

context