Troubleshooting
Common issues when integrating @gabriel-sisjr/react-native-background-location, organized as Symptom, Cause, Fix. If your issue is not listed, check the Example App, the GitHub Issues, and the related guide links at the end of each entry.
All code samples use granular named imports, matching the v0.14.0+ public API.
Android
Foreground service fails to start within 5-10s on Android 12+
The app crashes with ForegroundServiceDidNotStartInTimeException shortly after startTracking().
Cause. On Android 12+ (API 31+), a foreground service must call startForeground() within ~5-10 seconds of being started, otherwise Android kills the process. This most often happens when the host app takes too long to parse TrackingOptions or when the system is under load.
Fix.
- The library already calls
startForeground()immediately inonStartCommand()with a minimal notification and then replaces it onceTrackingOptionsare parsed -- you do not need to implement this yourself. - Make sure you declared the
FOREGROUND_SERVICEandFOREGROUND_SERVICE_LOCATIONpermissions inAndroidManifest.xml. See the Quick Start guide for permission setup. - Do not block the main thread between
startTracking()and receiving the first location update -- heavy synchronous JS work delaysonStartCommand(). - If you added custom notification icons/colors, verify the drawables exist and the hex color is valid; a failed resource lookup can silently delay the upgrade to the full notification.
Related: Platform Comparison
Duplicate location callbacks after crash recovery
After a process restart you receive two onLocationUpdate events per GPS fix, or the locations array grows at twice the expected rate.
Cause. When LocationService.onStartCommand() runs a second time -- via RecoveryWorker, START_REDELIVER_INTENT, or onTimeout() restart -- a new LocationProvider subscription is added on top of the existing one.
Fix.
- Both
FusedLocationProviderandAndroidLocationProvidercallremoveLocationUpdates()at the start ofrequestLocationUpdates()specifically to prevent this. If you are still seeing duplicates, upgrade to the latest version of the library. - If you forked or wrapped the provider, preserve the
removeLocationUpdates()call at the top ofrequestLocationUpdates(). - Verify you are not calling
startTracking()twice from JS (e.g. from auseEffectthat fires on every render).startTracking()is idempotent on the native side, but stacking JS callers is still a code smell.
Related: Crash Recovery guide, Debugging guide
Service restart loop
The foreground service restarts several times in a row, then stops entirely; subsequent startTracking() calls appear to do nothing.
Cause. LocationService ships with a restart loop detector: if the service is restarted more than 5 times within a 1-hour window, it refuses further restarts and clears the persisted tracking state. This prevents runaway battery drain when RecoveryWorker and START_REDELIVER_INTENT cooperate unexpectedly (e.g., after a series of process kills triggered by an OEM battery optimizer).
Fix.
- Wait ~1 hour or reboot the device to reset the restart counter.
- Check your logs for whatever is actually killing the service (usually an OEM battery optimizer, a
TASK_REMOVED+ stopWithTask interaction, orSecurityExceptionon background start). Fix the root cause before retrying. - Ensure the user has whitelisted your app in battery optimization settings -- see Battery Optimization for vendor-specific flows.
- Call
stopTracking()cleanly on app shutdown when possible;onDestroyresets the restart counter.
Related: Battery Optimization, Crash Recovery
iOS
Always permission is never granted
requestPermissions() resolves with location.hasPermission === true, but the permission stays at WhenInUse and background tracking stops as soon as the app is backgrounded.
Cause. iOS uses a two-step authorization flow. The system must first grant WhenInUse, and only after a separate call to requestAlwaysAuthorization() will the user see the "Allow Always" prompt. The LocationPermissionStatus.WHEN_IN_USE value counts as hasPermission: true for foreground tracking, which is why requestPermissions() returns true even when the escalation has not completed.
Fix.
- Use the
useLocationPermissionshook -- it handles the fullWhenInUsetoAlwaysescalation automatically on iOS viarequestLocationPermission(foregroundOnly: false). - Check
permissionStatus.location.status === LocationPermissionStatus.GRANTED(not justhasPermission) before relying on background tracking. - Ensure
NSLocationAlwaysAndWhenInUseUsageDescriptionandNSLocationWhenInUseUsageDescriptionare both present inInfo.plist-- iOS silently rejects the escalation if either string is missing. - The user must have approved
WhenInUseon a prior prompt; iOS will not show the "Always" dialog if permission is currently.denied.
import {
useLocationPermissions,
LocationPermissionStatus,
} from '@gabriel-sisjr/react-native-background-location';
function PermissionGate() {
const { permissionStatus, requestPermissions } = useLocationPermissions();
const canTrackInBackground =
permissionStatus.location.status === LocationPermissionStatus.GRANTED;
if (!canTrackInBackground) {
return <Button title="Grant Always" onPress={requestPermissions} />;
}
return <TrackingScreen />;
}
Related: iOS Setup, Permission Handling
didChangeAuthorization resolves the permission promise prematurely
On iOS the first requestPermissions() call returns immediately with a stale status (often WhenInUse) before the user has tapped anything in the system prompt.
Cause. iOS fires CLLocationManagerDelegate.didChangeAuthorization(_:) synchronously the moment a delegate is assigned to a CLLocationManager -- reporting whatever authorization currently exists. Without a guard, the native wrapper would treat this immediate callback as the user's response and resolve the promise too early.
Fix.
- The library handles this with the
shouldIgnoreNextAuthCallbackflag inLocationManagerWrapper.swift: the first delegate callback that arrives right after delegate assignment is discarded. If you are on the latest version, nothing is required. - If you extended
LocationManagerWrapperor instantiated your ownCLLocationManager, apply the same guard before completing your permission promise. - When debugging, log
didChangeAuthorizationinvocations in the delegate and confirm the first event after assignment is ignored.
Related: iOS Setup
Cross-Platform
Native module is not available on a simulator or JS-only test
Calling startTracking() (or any method) throws Native module is not available. Make sure the app was rebuilt after installing the package.
Cause. The TurboModule binary is not linked into the current runtime. This happens on bare JS test environments, Jest without mocks, iOS simulators where the pod is missing, or Android emulators without a rebuild after installing the package.
Fix.
- Rebuild the native app after installing the package:
- iOS:
cd ios && pod install && cd .. && yarn ios - Android:
yarn android(Gradle picks up the autolinked module)
- iOS:
- For probing whether the module is available without throwing, wrap calls in
try/catch. The library's geofencing methods use a throwing variant (assertNativeModuleAvailable()) and will raise a clear error. - If you see this only in unit tests, mock
react-native'sNativeModulesor provide a Jest setup that stubs the module. See the Testing guide for mock patterns.
import { isTracking } from '@gabriel-sisjr/react-native-background-location';
try {
const { active } = await isTracking();
console.log('Tracking active:', active);
} catch (err) {
console.warn('Native module not linked -- simulator or JS-only env.', err);
}
Related: Installation, iOS Setup
Locations seem to be missing after stopTracking()
After calling stopTracking(), getLocations(tripId) returns fewer points than you expected, or the last few points you saw on the live map are gone.
Cause. Both platforms use a batched async write pipeline to amortize DB I/O: up to 10 locations are buffered in memory and flushed to Room / Core Data whenever the buffer fills or a 5-second timer expires. stopTracking() force-flushes the buffer before returning, but a crash during shutdown could lose in-flight points.
Fix.
- Always
awaitstopTracking()before reading withgetLocations()-- the stop sequence flushes pending writes synchronously. - If you rely on real-time points from
useLocationUpdates, call its exposedrefreshLocations()afterstopTracking()returns to re-hydrate from the DB. - When in doubt, re-read from the DB -- the NativeEventEmitter stream is authoritative for UI but the DB is the persistence ground truth.
import {
stopTracking,
getLocations,
} from '@gabriel-sisjr/react-native-background-location';
await stopTracking();
const points = await getLocations(tripId); // DB is flushed by this point
Related: Real-Time Updates
Stale location events arrive after stopTracking()
You receive an onLocationUpdate a few seconds after calling stopTracking(), with coordinates from before the stop.
Cause. The underlying OS location subsystems (Fused, CLLocationManager) can queue a callback that is already mid-flight when you call stopTracking(). The library uses a stop token (SharedPreferences on Android, UserDefaults on iOS) that is written synchronously in the stop sequence; any late callback that arrives while the stop token is set is suppressed before emission.
Fix.
- Make sure you call
stopTracking()andawaitit -- synchronous persistence of the stop token is the core of the mechanism. - If you are seeing late emissions despite stopping, verify that you have only one
LocationServiceinstance and that you are not re-starting tracking in the same turn. - The stop token has a 60-second validity window on Android; if you immediately
startTracking()again, the new session correctly clears the token. - Do not disable the stop token check in forks -- dropping late events is intentional, not a bug.
Geofence transitions do not fire when tracking is stopped
You added geofences with addGeofences(), but ENTER/EXIT events only arrive while isTracking() is true.
Cause. The system's geofencing services rely on a warm GPS pipeline to detect region crossings. If location tracking is off and no other app is using location, the radio can cool down enough that transitions are delayed by several minutes, or missed entirely for small geofences.
Fix.
- The library runs a location heartbeat that keeps the GPS pipeline warm for passive geofence detection when tracking is not active. It auto-starts when geofences exist and tracking is off, and auto-stops when the last geofence is removed. No manual wiring required.
- Use a minimum radius of ~100 m on Android and ~150 m on iOS. Smaller geofences are unreliable on both platforms regardless of heartbeat.
- On iOS,
Alwayspermission is mandatory for background geofence monitoring -- see the "Always permission" entry above. - If transitions are delayed, check battery optimizer whitelisting (Battery Optimization) -- OEM doze can suppress geofence broadcasts even with a heartbeat.
Related: Geofencing, Geofencing Advanced, Battery Optimization
Build / Tooling
TurboModule Codegen fails with "Type not supported" or missing spec files
Building the iOS or Android app fails at the Codegen step, complaining about enum types, object arrays, or missing generated headers.
Cause. React Native Codegen does not support TypeScript enum types or typed object arrays in TurboModule specs. The library's spec uses plain string unions and JSON-serialized strings for complex objects specifically to satisfy these constraints. Codegen also requires the New Architecture to be enabled in the host app.
Fix.
- Ensure the host app has the New Architecture enabled:
- iOS:
RCT_NEW_ARCH_ENABLED=1 pod install(or setnewArchEnabled=trueinios/Podfile.properties.jsonfor Expo) - Android:
newArchEnabled=trueinandroid/gradle.properties
- iOS:
- Run a clean
pod installafter changing the flag -- Codegen outputs land inios/build/generated/ios/and must be regenerated. - Do not modify the library's
NativeBackgroundLocation.tsspec to use TypeScript enums or typed object arrays -- Codegen will reject them. - If the generated files are stale, run
yarn cleanin the library andyarn prepareto regenerate the published artifacts.
Related: iOS Setup
Still Stuck?
- Re-read the Quick Start and Installation guides to make sure nothing was skipped.
- Browse the Example App for a working reference.
- Search the GitHub Issues -- include full stack traces and the output of
npx react-native infowhen filing a new issue.