AI-powered crash analysis is now available on all plans — including Free.Read the crash analysis guide

Mobile CarPlay & Android Auto Crash Debugging Guide

NFNourin Mahfuj Finick··10 min read

When a mobile app crashes only while connected to a car's head unit, most teams are caught completely off guard. Device-only reproductions come back clean, but the moment the phone projects to CarPlay or Android Auto, the app dies. This guide walks through crash debugging for both in-vehicle platforms — Apple CarPlay on iOS and Android Auto on Android — covering scene and session lifecycles, entitlement and template constraints, audio focus conflicts, head-unit disconnect races, and the production observability strategy that turns these fleet-only failures into a routine fix. By the end you'll have a repeatable checklist for tracing the crashes that only ever appear in the car, and the instrumentation to catch them before your drivers notice.

Why in-car crashes are different

Crashes on the dashboard are not ordinary mobile crashes wearing a bigger screen. The execution environment is fundamentally different. On iOS, a CarPlay scene runs in a separate process from your main app, governed by its own lifecycle and its own watchdog. On Android, the projection surface is hosted by a Google Play Services process, and your app contributes a CarAppService whose Session can be created and destroyed without any Activity ever being visible. The result is a class of failure — constraint violations, lifecycle mismatches, and audio focus races — that never reproduces on a desk and only surfaces when a head unit connects.

There is also a hard resource dimension. In-car processes are subject to stricter memory and CPU budgets than a foreground phone app, and the system watchdog is far less patient when a head unit is waiting on your app to render a template. A slow root-template handshake that would merely drop frames on a phone becomes a watchdog kill in the car, reported as a crash whose stack trace points nowhere near the actual cause. Teams that only monitor crash counts without breadcrumbs spend days chasing these ghosts.

Because these surfaces are gated behind restricted capabilities, a surprising share of "crashes" are actually launch failures caused by missing entitlements or an unsupported template. Before chasing a segfault, verify the structural prerequisites. That single habit eliminates roughly a third of the in-vehicle bugs we see in the field.

CarPlay crash debugging on iOS

CarPlay is documented by Apple in the CarPlay framework reference, and the first thing to internalize is that a CarPlay scene is not your app's main scene. It has its own UISceneDelegate, its own connection options, and its own teardown path. Any code that conflates the two is a latent crash.

Scene lifecycle vs app lifecycle

The most common CarPlay crash is a delegate that assumes the main app scene is alive. When a user unplugs the phone, the CarPlay scene disconnects while your app continues running in the background. Code that holds a strong reference to a CPInterfaceController from the now-dead scene, then tries to push a template onto it, crashes with an over-released controller.

// CPInterfaceController is owned by the scene, not the app delegate
class CarPlaySceneDelegate: UIResponder, CPTemplateApplicationSceneDelegate {
    var interfaceController: CPInterfaceController?
 
    func templateApplicationScene(
        _ scene: CPTemplateApplicationScene,
        didConnect interfaceController: CPInterfaceController
    ) {
        // Retain the controller only for the lifetime of this scene
        self.interfaceController = interfaceController
        interfaceController.setRootTemplate(makeRootTemplate(), animated: true) { success, _ in
            if !success {
                // Surface the failure rather than force-pushing a stale template
                NSLog("// CarPlay root template rejected by the system")
            }
        }
    }
 
    func templateApplicationScene(
        _ scene: CPTemplateApplicationScene,
        didDisconnect interfaceController: CPInterfaceController
    ) {
        // Null it out so no other thread can touch a dead controller
        self.interfaceController = nil
    }
}

The fix pattern is to treat every CPInterfaceController reference as scoped to the scene connection, null it on didDisconnect, and guard every template push with a weak self and an availability check. For a deeper look at how system-initiated teardown masquerades as a crash, our guide on mobile app process death and OS kill detection maps the same teardown signals to their code-level equivalents.

Delegate deallocation races

A subtler variant of the lifecycle crash happens when a delegate object is deallocated while the system still holds a weak (or, historically, an unretained) reference to it. CPInterfaceController fires its delegate callbacks on an internal queue, so a delegate that is released on the main thread while a template action is in flight produces the classic "message sent to deallocated instance" crash. The defense is to keep delegate objects alive for the entire duration of the scene connection, and to set delegate = nil explicitly in didDisconnect before releasing anything.

Entitlement gating and launch failures

Every CarPlay app must declare the correct entitlement or the scene simply never connects — and the failure often logs as a generic launch crash. Navigation apps need com.apple.developer.carplay-maps, audio apps need com.apple.developer.carplay-audio, and communication apps need com.apple.developer.carplay-messaging. Apple documents these in the CarPlay entitlement reference.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.developer.carplay-audio</key>
    <true/>
</dict>
</plist>

If the entitlement is present but the scene still won't launch, verify the template your root view uses is in the app's declared supported categories. An audio app cannot present a map template, and attempting it fails the scene handshake with an error that looks nothing like a template mismatch.

Audio focus and remote command races

CarPlay audio apps live and die by MPNowPlayingInfoCenter and MPRemoteCommandCenter. When a head unit connects, both the phone and the car can attempt to become the now-playing source. A remote command handler that mutates shared playback state without synchronization crashes when a command arrives mid-disconnect.

// Register handlers and always respond so the system doesn't time out
func setupRemoteCommands() {
    let center = MPRemoteCommandCenter.shared()
    center.playCommand.addTarget { [weak self] _ in
        self?.player?.play()
        return .success // Always return .success or the command stalls
    }
    center.pauseCommand.addTarget { [weak self] _ in
        self?.player?.pause()
        return .success
    }
}

The failure mode to watch for is a handler returning .commandFailed during a head-unit transition, which can cascade into a watchdog termination. If audio is your surface, our mobile audio crash debugging guide covers focus arbitration and Bluetooth routing crashes in depth.

Android Auto crash debugging

Android Auto apps are built on the Android for Cars library, and the entry point is a CarAppService that the system binds to when projection starts.

CarAppService and Session lifecycle

The most frequent Android Auto crash is a Session that assumes it will outlive the head-unit connection. The system can destroy and recreate your Session when the user unplugs, switches projection modes, or the car's screen rotates. Any coroutine or callback captured inside the Session that touches the UI after destruction throws an IllegalStateException.

class MediaCarAppService : CarAppService() {
    override fun createHostValidator(): HostValidator =
        HostValidator.ALLOW_ALL_HOSTS
 
    override fun onCreateSession(): Session {
        // Return a fresh Session; never cache a single Session instance
        return object : Session() {
            override fun onCreateScreen(intent: Intent): Screen {
                return ScreenBuilder().build()
            }
        }
    }
}

The fix is to scope all state to the Session, cancel lifecycle-aware coroutines on onDestroy, and never keep a Session reference in an application-level singleton. For background audio, use a MediaSession tied to a foreground service, not to the Session's transient scope.

CarAppApiLevel gating

The Android Auto template set is versioned through CarAppApiLevel. Calling a method introduced in a newer API level on an older head unit throws UnsupportedOperationException, which many teams misread as a crash. Google's Android for Cars documentation recommends gating every template behind an API level check.

// Gate features behind CarAppApiLevel before touching newer templates
if (CarAppApiLevel.getCarAppApiLevel() >= CarAppApiLevel.LEVEL_4) {
    // GridTemplate and other LEVEL_4+ features are safe here
    buildGridTemplate()
} else {
    // Fall back to a ListTemplate for older head units
    buildListTemplate()
}

Template constraints

Beyond API-level gating, Android Auto imposes structural limits on templates that are easy to violate at runtime. GridTemplate and ListTemplate cap the number of items they can render, and exceeding that cap is rejected rather than silently truncated. A GridTemplate that is rebuilt with a larger item set mid-drive can throw where the same code ran fine with a smaller list. Validate item counts against the documented limits before constructing a template, and always provide a fallback list for head units that reject the grid.

MediaSession callbacks after disconnect

Android Auto media apps register a MediaSession so the head unit can drive playback. When projection ends, the system may deliver a final flurry of callbacks to a session that is already releasing. A callback that calls notify() on a dead PlaybackStateCompat or touches a released MediaPlayer crashes during disconnect.

// Release in the correct order and null references on disconnect
override fun onDestroy() {
    mediaSession?.setActive(false)
    mediaSession?.release()
    mediaSession = null
    player?.release()
    player = null
    super.onDestroy()
}

The rule of thumb: teardown must be idempotent and order-safe, because the system will call it more than once during a projection handoff.

Driving optimization and permission requirements

Android Auto enforces driving optimization requirements that restrict templates to glanceable, low-interaction content. A template that exceeds the allowed item count or uses a restricted template type is rejected — and depending on the host's behavior, that rejection can surface as an exception rather than a graceful fallback. Keep lists short, avoid nested scrolling, and never present an interactive keyboard on the car screen.

Tracing head-unit-only crashes in production

The hard part about in-vehicle crashes is that they reproduce only with a physical head unit attached. This is where breadcrumb-style observability earns its keep. By instrumenting the key connection events — CarPlay scene connect and disconnect, Android Auto onCreateSession and onDestroy, audio focus gain and loss — you can reconstruct the exact sequence that preceded a crash without ever reproducing it locally.

https://bugspulse.com provides exactly this: lightweight breadcrumbs, session-level metadata, and cross-platform aggregation that let you see, in one timeline, that a CarPlay crash always follows an audio focus loss, or that an Android Auto crash clusters on a specific head-unit firmware string. Instead of guessing which of a dozen callbacks fired last, you see the breadcrumb trail that ends at the fault.

A practical production workflow looks like this: log a breadcrumb at every scene or session boundary, attach the head-unit identifier and OS version as custom attributes, and treat any crash whose breadcrumbs show a disconnect/reconnect within the last two seconds as an in-car lifecycle bug rather than a general defect. That classification alone routes the ticket to the right owner and cuts mean time to resolution dramatically.

Testing against real head units remains the gold standard, but it is expensive and slow. The pragmatic middle path is to pair a small fleet of desktop head-unit simulators with breadcrumb correlation in production — the simulator catches structural and template issues before release, while breadcrumbs catch the firmware-specific races that only a real vehicle exposes.

A repeatable checklist

When an in-car crash lands on your board, work through this sequence before opening the debugger. First, confirm the entitlement (CarPlay) or the API level and manifest declaration (Android Auto) are correct. Second, verify every template push and screen build is gated and guarded against a null or destroyed controller. Third, audit your audio stack for unhandled remote-command and media-session callbacks during disconnect. Fourth, reproduce the teardown path by toggling projection off and on in rapid succession — the disconnect/reconnect race is the single highest-yield reproduction technique. Fifth, confirm your breadcrumbs captured the full boundary sequence so the next occurrence is diagnosable without a car in the room.

In-vehicle surfaces are the frontier of mobile crash debugging. They combine the strictest constraints — glanceable templates, separate processes, restricted entitlements — with the least forgiving failure mode: a crash that blanks the driver's screen. Instrument the lifecycle boundaries, gate every feature, and you'll turn a class of bugs that only appears at sixty miles per hour into a class you catch in the first pull request.

If you want to see these head-unit-only crashes in production before your users do, start with a free account at app.bugspulse.com/register and ship your first in-car release with full breadcrumb visibility from day one.