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

Debug Haptics & Taptic Engine Crashes on iOS & Android

NFNourin Mahfuj Finick··8 min read

Haptic feedback is one of the most overlooked crash surfaces in modern mobile apps. Every time an iPhone fires its Taptic Engine or an Android device spins up a vibration motor, your code is crossing into a hardware-backed subsystem with its own lifecycle, threading rules, and platform quirks. When a CHHapticEngine stops unexpectedly or a Vibrator call throws on a low-API device, the result is often a crash that ships silently because simulators and emulators cannot reproduce real actuator behavior. This guide walks through the most common haptics and Taptic Engine crash patterns on iOS and Android, and shows how to catch them before your users feel the impact.

Why Haptic Crashes Slip Into Production

The core reason haptic bugs reach production is environmental blindness. The iOS Simulator does not expose the Taptic Engine, and most Android emulators have no vibrator at all, so the code path that calls impactOccurred() or vibrate() is simply never exercised during development or CI. Compounding the problem, haptics are usually fire-and-forget side effects: developers wrap them in try! or ignore the return value, assuming the hardware will always be there.

A second reason is lifecycle coupling. On iOS, Core Haptics lives inside the same media pipeline as audio, so an audio session interruption or an app entering the background can silently stop the haptic engine. On Android, the Vibrator service is obtained through the system service registry, which can return null or throw a SecurityException when the VIBRATE permission is missing. Neither failure is visible in the UI, which is exactly why they need proactive monitoring.

iOS: Core Haptics and the Taptic Engine

Apple's modern haptics stack is Core Haptics, centered on the CHHapticEngine class. The engine is not a trivial singleton: it has an explicit start/stop lifecycle, a reset handler, and a stopped handler, and it can be torn down by the system at any time.

The Force-Try Trap

The single most common haptics crash on iOS is force-unwrapping throwing initializers. CHHapticEngine(), start(), and playPattern(from:) all throw, and a try! on any of them converts a recoverable error into a hard crash:

let engine = try! CHHapticEngine()
try! engine.start()
try! engine.playPattern(pattern, from: event)

When the device lacks haptic support, or the audio session is unavailable, start() throws and the app crashes. The fix is a do/catch block that degrades gracefully instead:

do {
    let engine = try CHHapticEngine()
    engine.isAutoShutdownEnabled = true
    try engine.start()
} catch {
    // Log to your crash reporter and continue without haptics.
}

Run-Loop and Reset-Handler Pitfalls

CHHapticEngine must be initialized on a thread with an active run loop; creating it on a background queue without one causes start() to fail. Apple recommends initializing the engine on the main thread or dispatching to a queue that has a run loop. The engine also has two handlers you must set: stoppedHandler (fired when the system stops the engine) and resetHandler (fired when the engine resets). If you ignore resetHandler, the engine stays stopped after a background/foreground transition and every subsequent playPattern call fails or crashes.

Always check hardware support first with CHHapticDeviceCapability.supportsHaptics before constructing an engine. Because haptics are tied to the audio session, you should also monitor AVAudioSession interruption notifications — when audio is interrupted, your haptic engine stops with it. This is a close cousin of the audio crash patterns we covered earlier, since both share the same media pipeline.

UIFeedbackGenerator Family

For simpler "tap" feedback, UIKit provides UIImpactFeedbackGenerator, UISelectionFeedbackGenerator, and UINotificationFeedbackGenerator. These are lighter weight but have their own rules: they must be prepared with prepare() before the triggering action, they must be triggered on the main thread, and the generator object must stay alive until impactOccurred() is called. Deallocating the generator mid-feedback or calling it from a background thread is undefined behavior that can crash. The Human Interface Guidelines recommend creating these lazily and reusing them rather than allocating one per tap.

CHHapticPattern and AHAP Validation

Advanced haptic designs are described with an AHAP (Apple Haptic and Audio Pattern) dictionary or JSON file, which you load through CHHapticPattern(events:parameters:) or CHHapticPattern(contentsOf:). Both initializers throw when the pattern is malformed — a mistyped key, an out-of-range parameter value, or a missing Event array all produce an exception. Force-trying these initializers is the same trap as the engine, but harder to spot because the pattern may only be invalid on certain devices or OS versions.

CHHapticEventParameter values such as HapticIntensity and HapticSharpness are only valid in the range 0.0 to 1.0, and CHHapticDynamicParameter values have their own bounds. Passing a value outside those ranges throws an error at pattern-creation time. Validate your AHAP content once at load, catch the error, and fall back to a simple built-in pattern rather than crashing on a hand-authored JSON typo.

Android: Vibrator, VibratorManager, and VibrationEffect

Android's haptics story is fragmented across API levels, which is the root of most crashes. The classic Vibrator class has been reworked several times, and the "obvious" API is deprecated.

Null Service and Permission Crashes

The vibrator service is fetched from the context:

val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator

On devices without a vibrator — or when the context is already destroyed — getSystemService can return null, and a direct cast to Vibrator throws a NullPointerException. Always use a safe cast and guard with Vibrator.hasVibrator(). Separately, calling vibrate() without the VIBRATE permission declared in the manifest throws a SecurityException. The permission is declared in the Android manifest.

VibrationEffect and API-Level Mismatches

On Android 8.0 (API 26) and later, vibrate(long) is deprecated in favor of VibrationEffect. The classic crash is calling VibrationEffect.createOneShot() on a pre-26 device, which throws NoSuchMethodError. Wrap all effect creation in a Build.VERSION.SDK_INT check and fall back to the deprecated call on old devices.

VibrationEffect.createWaveform(long[] timings, int[] amplitudes, int repeat) is a frequent source of IllegalArgumentException. The amplitudes array must contain exactly timings.length - 1 entries, each amplitude must be between 0 and 255, and mismatched lengths crash at runtime. Validate the arrays before calling:

if (Build.VERSION.SDK_INT >= 26) {
    val timings = longArrayOf(0L, 120L, 200L)  // 3 timings
    val amplitudes = intArrayOf(0, 180)        // must be timings.size - 1 = 2
    vibrator?.vibrate(VibrationEffect.createWaveform(timings, amplitudes, -1))
} else {
    @Suppress("DEPRECATION")
    vibrator?.vibrate(400L)
}

Note the amplitudes array is one element shorter than timings, and the repeat index is -1 to avoid looping. The two most common crashes here are a length mismatch and a repeat index that points past the end of the array.

VibratorManager and HapticFeedbackConstants

On Android 12 (API 31) and later, multi-vibrator devices expose the VibratorManager, retrieved via context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE). Calling the old VIBRATOR_SERVICE path on these devices still works but may not address the correct motor. For UI affordances, HapticFeedbackConstants pairs with view.performHapticFeedback(HapticFeedbackConstants.CONFIRM). This path is safe only when the view has haptic feedback enabled, so guard with view.isHapticFeedbackEnabled.

Cross-Platform Failure Modes

Across both platforms, the same three failure classes keep showing up in production crash reports:

  • Lifecycle: engines that stop silently and are never restarted, then throw on the next call.
  • Version skew: new APIs called on old OS versions, or deprecated APIs called on new ones.
  • Environment: simulator/emulator paths that never execute, so the first real device run is the first test.

Haptics also share the broader sensor and actuator debugging concerns, since the vibration motor is ultimately a hardware component managed by the OS. Power and thermal throttling add one more wrinkle: on low battery or a hot device, the OS may reduce or disable vibration intensity, which changes timing behavior in ways your code should not assume are fixed.

Debugging Haptic Crashes in the Wild

Because haptic crashes rarely reproduce in development, production telemetry is what actually leads you to the root cause. Start with the exception type: NSInternalInconsistencyException and the com.apple.CoreHaptics error domain on iOS, versus NullPointerException, SecurityException, IllegalArgumentException, and NoSuchMethodError on Android. Each one points at a different root cause, so the exception name alone narrows the search dramatically.

Next, capture the environment. The device model and API level tell you whether you are dealing with a device that lacks haptic hardware or an OS that predates VibrationEffect. The audio session state on iOS tells you whether an interruption stopped the engine. The breadcrumbs leading up to the crash — which screen, which action, which haptic was playing — turn a vague report into a reproducible scenario. Attach all of this to the crash, and route haptic errors through the same pipeline you use for the rest of the app so a "harmless" side-effect failure is never silently dropped.

A Defensive Haptics Strategy

The reliable pattern is to centralize all haptics behind a single wrapper that absorbs every failure mode. On iOS, that wrapper owns the CHHapticEngine, restarts it in resetHandler, catches every throw, and checks supportsHaptics before doing any work. On Android, it checks Build.VERSION.SDK_INT, uses a safe cast for the service, validates waveform arrays, and declares the VIBRATE permission once. Wrap every haptic call in a try/catch, log the failure with a breadcrumb, and degrade to no-op rather than crash. A haptic that fails to play is a minor UX regression; a crash that kills the whole session is not.

That is where a crash-monitoring platform earns its keep. Haptic failures are exactly the kind of low-frequency, environment-specific errors that surface days after a release, on one device model, under a specific audio session state. Capturing the stack trace, the device model, the API level, and the breadcrumbs leading up to the failure turns an unreproducible "haptics broke" ticket into a fix you can ship in an afternoon. Bugspulse gives you that context with privacy-first, zero-PII crash reporting built for mobile teams.

Ready to catch haptic crashes before your users do? Sign up for Bugspulse and get crash visibility across every iOS and Android device in your fleet.