Repository: Kotlin/kotlinx.coroutines
File: kotlinx-coroutines-core/common/src/JobSupport.kt (Kotlin)

Code before:
     */
    protected val completionCause: Throwable?
        get() = when (val state = state) {
            is Finishing -> state.rootCause
                ?: error("Job is still new or active: $this")
            is Incomplete -> error("Job is still new or active: $this")
            is CompletedExceptionally -> state.cause
            else -> null
        }

    /**
     * Returns `true` when [completionCause] exception was handled by parent coroutine.
     */
    protected val completionCauseHandled: Boolean
        get() = state.let { it is CompletedExceptionally && it.handled }

    public final override fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle =
        invokeOnCompletionInternal(
            onCancelling = false,
            invokeImmediately = true,
            node = InvokeOnCompletion(handler),
        )

    public final override fun invokeOnCompletion(onCancelling: Boolean, invokeImmediately: Boolean, handler: CompletionHandler): DisposableHandle =
        invokeOnCompletionInternal(
            onCancelling = onCancelling,
            invokeImmediately = invokeImmediately,
            node = if (onCancelling) {
                InvokeOnCancelling(handler)
            } else {
                InvokeOnCompletion(handler)
            }
        )

    internal fun invokeOnCompletionInternal(
        onCancelling: Boolean,
        invokeImmediately: Boolean,
        node: JobNode
    ): DisposableHandle {
        assert { onCancelling == node is JobCancellingNode }
        node.job = this
        loopOnState { state ->
            when (state) {
                is Empty -> { // EMPTY_X state -- no completion handlers
                    if (state.isActive) {
                        // try move to SINGLE state
                        if (_state.compareAndSet(state, node)) return node
                    } else
                        promoteEmptyToNodeList(state) // that way we can add listener for non-active coroutine
                }
                is Incomplete -> {

Review comment (performance):
Looking at that (and use-sites), `JobCancellingNode` starts looking almost like a marker interface.
Here is the gist of an alternative approach:

* Replace these two (three, `JobCancelling` as well) types with a single type, `InvokeOnTbdNaming`
* Add a (non-virtual) property `JobNode.invokeOnCancelling`
* Replace virtual classes checks with `node.invokeOnCancelling`

Pros:
* Two classes less, smaller hierarchy
* More straightforward code (i.e. `InvokeOnTbdNaming(handler, onCancelling)`)
* No `is` checks -- some small perf improvements

Cons:

* Might take an extra property for the `_invoked` (though it can be optimized away with sneaky use of `onCancelling`)
* Potentially it might be less straightforward to maintain

WDYT?

Code after:
     */
    protected val completionCause: Throwable?
        get() = when (val state = state) {
            is Finishing -> state.rootCause
                ?: error("Job is still new or active: $this")
            is Incomplete -> error("Job is still new or active: $this")
            is CompletedExceptionally -> state.cause
            else -> null
        }

    /**
     * Returns `true` when [completionCause] exception was handled by parent coroutine.
     */
    protected val completionCauseHandled: Boolean
        get() = state.let { it is CompletedExceptionally && it.handled }

    public final override fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle =
        invokeOnCompletionInternal(
            invokeImmediately = true,
            node = InvokeOnCompletion(handler),
        )

    public final override fun invokeOnCompletion(onCancelling: Boolean, invokeImmediately: Boolean, handler: CompletionHandler): DisposableHandle =
        invokeOnCompletionInternal(
            invokeImmediately = invokeImmediately,
            node = if (onCancelling) {
                InvokeOnCancelling(handler)
            } else {
                InvokeOnCompletion(handler)
            }
        )

    internal fun invokeOnCompletionInternal(
        invokeImmediately: Boolean,
        node: JobNode
    ): DisposableHandle {
        node.job = this
        loopOnState { state ->
            when (state) {
                is Empty -> { // EMPTY_X state -- no completion handlers
                    if (state.isActive) {
                        // try move to SINGLE state
                        if (_state.compareAndSet(state, node)) return node
                    } else
                        promoteEmptyToNodeList(state) // that way we can add listener for non-active coroutine
                }
                is Incomplete -> {
                    val list = state.list
                    if (list == null) { // SINGLE/SINGLE+
                        promoteSingleToNodeList(state as JobNode)
                    } else {