Task not firing in a Notification Closure

Context:

Consider this code, where an object subscribes to a notification and handles it on the main queue:

@MainActor
final class ModelController
{
    init() 
    {
        NotificationCenter.default.addObserver(forName: .appTerminationRequestedNotification, object: nil, queue: .main) { note in 
            Task { @MainActor in 
                print("calling task closure.")
             }
        }
    }
}

This Task never fires. The notification closure is called correctly, but the Task itself appears to be skipped.

(Even though we've explicitly told the notification handler to run on the main queue, the compiler can't reason about that, so it's necessary to redundantly dispatch to the MainActor when you must access state.)

A Working Alternative:

This works just fine:

NotificationCenter.default.addObserver(forName: .appTerminationRequestedNotification, object: nil, queue: .main) { note in 
    MainActor.assumeIsolated {
       print("This works just fine.")
    }
}

The notification is posted here. The app in question is a SwiftUI Lifecycle app using @NSApplicationDelegateAdaptor(AppDelegate.self)

///  Posed when the user attempts to quit the app so that ModelController can gracefully close a database.
static let appTerminationRequestedNotification = Notification.Name("com.example.Foo.appTerminationRequested")

class AppDelegate: NSObject, NSApplicationDelegate
{
    func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply
    {
        NotificationCenter.default.post(name: .appTerminationRequestedNotification, object: self)
        return .terminateLater
    }
}

Question:

Why? I've used Task { @MainActor in } for other notification handlers and have never had an issue. I'm trying to determine if this behavior is new in macOS 26 or is somehow related to the termination flow, etc.

Maybe relevant: per the documentation, if you return .terminateLater from applicationShouldTerminate, the run loop is run in NSModalPanelRunLoopMode mode. Perhaps main-actor Tasks are not executed when the run loop is run in that mode.

1 Like

Couldn't Task { @MainActor in be translated to something like this under the hood "effectively":

    // some unknown or non isolated domain
    if Thread.isMain {
        // do something (quick path)
    } else {
        DispatchQueue.main.async {
            // do something (slow path)
        }
    }

if you return .terminateLater from applicationShouldTerminate , the run loop is run in NSModalPanelRunLoopMode mode. Perhaps main-actor Tasks are not executed when the run loop is run in that mode.

Ah, that definitely has to be the cause. I forgot about the runloop mode change. Thanks!