You ship a Rork-generated app, tap the "favorite" button in Expo Go, and feel a satisfying tick under your fingertip. Then you push to TestFlight and the same button feels completely dead. I burned half a day on this exact pattern last week.
I have been shipping wallpaper and well-being apps as an indie developer since 2014. When I cross-reference AdMob session data against user behavior, apps with working haptics show 10–20% longer average session times than apps where haptics are silently broken. The feedback itself is only a few dozen milliseconds, but without it, taps lose their "this happened" feel. Shipping an app with broken haptics without noticing is the kind of bug that quietly erodes review scores for months.
The frustrating thing about expo-haptics is that the JavaScript surface is tiny, which means that when it does not work, the cause is almost always outside the call. Here are five reasons, in the order I check them.
Three things to isolate first
Before touching any code, confirm these. They cut the search space in half.
- Is the device physically silent, or is the vibration so weak you do not notice?
- Is only iOS broken, only Android, or both?
- Does it work in Expo Go but break in EAS Build / TestFlight?
The iOS Simulator never fires haptics — that is by design, not a bug. I once spent hours debugging on Simulator after switching to a new Apple Silicon Mac. Always confirm on physical hardware.
Cause 1: System haptics disabled on the iPhone
The first suspect is the user's (or your own) iPhone setting. If Settings → Sounds & Haptics → System Haptics is off, expo-haptics calls silently no-op. There is no error and no callback — the call just disappears.
This is intentional. Apple's HIG says haptics must respect the OS setting, and there is no public API to force-fire when the user has disabled the system. When users report "no vibration" in App Store reviews, this setting accounts for more than half the cases in my experience.
I once panicked over a three-star review complaining about a dead app — turned out the reviewer had system haptics off. Adding a single line of in-app help text ("Haptics respect your iOS system settings") cuts these reports dramatically.
Cause 2: Low Power Mode throttles the Taptic Engine
When iOS enters Low Power Mode, the Taptic Engine is throttled to save battery. Haptics.impactAsync may produce only a click sound, or do nothing at all. Apple does not document this loudly, but the behavior is tied to UIDevice.current.isLowPowerModeEnabled. From React Native, you can read the same flag through expo-battery.
import * as Haptics from 'expo-haptics';
import * as Battery from 'expo-battery';
async function softTap() {
const isLowPower = await Battery.isLowPowerModeEnabledAsync();
if (isLowPower) {
// Expected: skip haptics, fall back to a visual confirmation
return;
}
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}Pair the haptic with a brief color animation on the UI element. That way, even in Low Power Mode, the user still gets a "tap registered" cue. In my wallpaper apps, every successful save fires both a haptic and a color flash, so the interaction feels intact even when the engine is silenced.
Cause 3: VIBRATE permission missing on Android
On Android, expo-haptics uses the VIBRATE permission internally. Expo SDK 50+ adds it automatically, but if you have manually narrowed android.permissions in app.json, you may have accidentally dropped it.
{
"expo": {
"android": {
"permissions": ["VIBRATE"]
}
}
}If you explicitly list other permissions but forget VIBRATE, the haptic call is a no-op on Android. Confirm the runtime permission set with adb shell dumpsys package <package-name> | grep VIBRATE. The trickiest case I have hit is a preview build that worked, followed by a production build that silently dropped the permission because of a different build profile.
Cause 4: Forgetting to await before navigating
A common pattern I see in Rork-generated code: calling Haptics.impactAsync without await and immediately navigating away.
// ❌ The screen transition cancels the haptic
<Pressable onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
router.push('/detail');
}}>impactAsync returns a Promise. When you push a new screen before it resolves, the activity loses focus and the engine cancels the queued vibration.
// ✅ Fire haptic, then navigate
<Pressable onPress={async () => {
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
router.push('/detail');
}}>The actual delay is only a few dozen milliseconds, but the perceived difference is enormous. Sticking to the rule "haptic first, navigate second" eliminates a surprising number of "haptics broken on prod" reports.
Cause 5: Rapid taps overflow the haptic queue
The haptic engine maintains an internal queue. Firing too many vibrations in rapid succession overflows the queue, and subsequent calls are silently dropped. The classic symptom is a swipeable card UI where the haptics work for the first second and then go dead.
A tiny throttle solves it.
import * as Haptics from 'expo-haptics';
let lastHapticAt = 0;
const HAPTIC_MIN_INTERVAL = 80; // milliseconds
export async function throttledLightImpact() {
const now = Date.now();
if (now - lastHapticAt < HAPTIC_MIN_INTERVAL) return;
lastHapticAt = now;
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}My wallpaper apps were trying to fire haptics roughly eight times per second when users swiped quickly through thumbnails. Adding an 80 ms throttle made the feel sharper, not duller — the engine could keep up cleanly instead of dropping every other tap.
Pre-release checklist
Boil the above into a checklist you run before shipping:
- Confirm physical vibration on both iOS and Android hardware
- Test with iOS system haptics both on and off
- Verify the Low Power Mode fallback path
- Check there is no rapid-tap stall (80–100 ms throttle recommended)
- Make sure all haptic calls
awaitbefore navigation
I run these five checks during every Internal Testing pass in TestFlight. Since adopting that habit, haptic-related review complaints have basically dropped to zero. It is an unsexy area of polish, but how carefully you handle it shapes the overall feel of the app more than people realize.
Thanks for reading — I hope your apps feel just a little more alive after this.