Diagnosing Carbon Event Manager Loop Failures in Legacy macOS Apps
A diagnostic guide to Carbon Event Manager event-loop failures in legacy macOS apps: match freeze and dropped-event symptoms to mask, dispatch, and handler-registration causes, then verify with timed Apple-event delivery.
15 Jun 2026, 07:16 UTC

A legacy Carbon app that freezes when it receives an Apple event, ignores menu clicks after a modal dialog, or silently drops high-level events almost always has a broken event loop — not broken handlers. The useful takeaway: in Carbon, events are only delivered if three things line up — the event mask you wait on, the handler you installed, and the loop actually yielding. A mismatch in any one produces a silent failure, because Carbon does not raise errors for undelivered events; they simply never arrive.
This guide applies to apps built against the Carbon Event Manager (HIToolbox), typically targeting macOS 10.4 through 10.14. Note upfront: Carbon UI APIs were deprecated in macOS 10.15 and are unsupported for new development. These diagnostics are for maintaining existing 32-bit-era or legacy 64-bit Carbon code, and behavior on current macOS versions should be verified on the oldest OS you still support.
Recognizable conditions
Start by matching what you observe to a symptom class:
- Apple events arrive but nothing happens. The app logs nothing, the handler never runs, and there is no error returned to the sender.
- The UI freezes for seconds at a time, then catches up in a burst — queued clicks and keys replay all at once.
- Some event types work and others never fire, even though handlers are installed for both.
- The app hangs only while a modal dialog or menu is open.
Cause and diagnostic table
| Symptom | Likely cause | First check |
|---|---|---|
| Apple events silently dropped | kHighLevelEventMask missing from the event mask passed to ReceiveNextEvent/GetNextEvent | Inspect the mask argument at the loop's wait call |
| Apple events dropped, mask correct | No ProcessHighLevelEvent (or AEProcessAppleEvent) call on receipt | Trace what the loop does with kHighLevelEvent class events |
| One event kind never fires | InstallEventHandler registered for the wrong event class/kind pair | Compare the EventTypeSpec list against the events you expect |
| Burst-then-freeze UI | Loop does heavy work without yielding; pending events starve | Look for long synchronous work between wait calls |
| Hang during modal UI | Sleep or blocking call inside modal filtering blocks window/control updates | Audit modal filter and any WaitNextEvent sleep values |
Ordered checks
Work through these in order; each is cheap and rules out a layer.
- Confirm the wait mask covers what you expect. Find the central loop call, e.g.
ReceiveNextEvent(0, NULL, kEventDurationForever, true, &event). A zero type list means all event classes — that is usually what you want. If you instead use the classicGetNextEvent/WaitNextEvent, verify the mask includeskHighLevelEventMask; without it Apple events are never pulled from the queue. - Confirm receipt dispatches to the handler. In a classic loop, a high-level event must be passed to
AEProcessAppleEvent(older code:ProcessHighLevelEvent). In a Carbon Event Manager loop,SendEventToEventTargetmust target the dispatcher or an object with the right handler installed. - Audit every
InstallEventHandlercall. Each takes an array ofEventTypeSpec— a class/kind pair such as{ kEventClassAppleEvent, kEventAppleEvent }. A handler installed forkEventClassCommandwill never see an Apple event, and Carbon gives no warning. - Check for starvation. Time one loop iteration. If any iteration can exceed roughly 100 ms of synchronous work, pending user and high-level events queue up and the UI appears frozen. Break the work up or move it off the main thread.
- Check modal paths separately. Menu tracking and modal dialogs run their own nested loops. Handlers installed only on a window target may not be reachable during modality; blocking sleeps in a modal filter stall control redraws.
Fixes tied to findings
Missing mask coverage: widen the mask. For the classic loop:
// Classic loop: must include high-level events or Apple events never arrive
EventRecord event;
WaitNextEvent(everyEvent, &event, 0, NULL); // not a hand-built partial mask
if (event.what == kHighLevelEvent)
AEProcessAppleEvent(&event); // requires Apple Event Manager handlers installedRun this in the app's main thread; no special permissions are needed. The risk of switching from a narrow mask to everyEvent is that you now receive event types your loop doesn't handle — verify the dispatch chain has a default path that discards unknown events rather than spinning.
Handler never fires: correct the EventTypeSpec. Log the class and kind of undelivered events by installing a catch-all handler on the event dispatcher target and printing GetEventClass/GetEventKind for anything unexpected. Then register the handler for the pair you actually observed.
Starvation: pass a finite timeout (kEventDurationNoWait for polling plus a short sleep, or a small duration) instead of doing work inline, and chunk long tasks across iterations. Verify by logging the timestamp of each delivered Apple event in a test run; gaps over ~100 ms during load indicate remaining starvation.
Verification
Rebuild and send a known Apple event (for example an odoc open-documents event via AppleScript) while logging handler entry. Confirm the handler runs and the main loop continues processing input during delivery. For a broader audit, run nm or otool -L on the binary to list linked Carbon frameworks, and cross-reference the Carbon entry points you call against Apple's deprecation list before assuming behavior on newer macOS.
Escalation criteria
Stop patching the loop and escalate to a re-architecture discussion when: the app must run on macOS 10.15 or later as a primary target (Carbon UI is deprecated and increasingly fragile there); you find Carbon and Cocoa event loops interleaved in the same process, which commonly deadlocks or drops events and resists point fixes; or the event mask must cover custom or future event types you cannot enumerate — a sign the design depends on behavior Carbon never guaranteed. At that point the durable fix is migrating the event handling to AppKit, not another loop tweak.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.