Required Reading
Plugin Version
5.1.1
Mobile operating-system(s)
Device Manufacturer(s) and Model(s)
iPhone 15 & iPhone 14 Plus
Device operating-systems(s)
iOS 26.3.1
React Native / Expo version
"react-native": "0.81.5", "expo": "~54.0.32",
What happened?
Your Environment
- Plugin version:
5.1.1
- Platform: iOS
- OS version: iOS 26.3.1 (multiple users, also seen on earlier iOS 17/18 builds)
- Device manufacturer / model: iPhone 14 Plus and others
- React Native version (
react-native --version): 0.76+
- Plugin config: Standard production setup with
autoSync: true, custom HTTP endpoint via url + headers
Expected Behaviour
When BackgroundGeolocation.sync() fails (HTTP timeout, server error, network drop), the JS promise should reject with a normal Error object that can be caught with try/catch.
Actual Behaviour
The app hard-crashes natively (process killed by iOS) with the following exception:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException',
reason: '-[__NSCFString code]: unrecognized selector sent to instance 0x600000e6aa00'
Stack trace (relevant frames):
0 CoreFoundation __exceptionPreprocess
1 libobjc.A.dylib objc_exception_throw
3 CoreFoundation ___forwarding___
5 React RCTJSErrorFromCodeMessageAndNSError + 844
7 App __39-[RNBackgroundGeolocation sync:reject:]_block_invoke_2 + 116
8 App __30-[TSHttpService finish:error:]_block_invoke.163 + 264
The JS-side try/catch cannot catch this — the crash happens in the native reject callback before the promise reaches the JS bridge.
Steps to Reproduce
Hard to reproduce on demand, but the pattern is:
- App is in foreground or just resumed
- Some points are queued in the local SQLite buffer
BackgroundGeolocation.sync() is called (manually or via autoSync)
- The HTTP layer (
TSHttpService) fails for any reason (timeout, 5xx, connection drop, slow Wi-Fi)
- App crashes immediately
We have 3 reproducible TestFlight feedbacks from the same user on iOS 26.3.1 over the past 10 days, and a clean Xcode crash report attached below.
Root Cause Analysis
Looking at RNBackgroundGeolocation.mm line 396–403:
RCT_EXPORT_METHOD(sync:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
{
[locationManager sync:^(NSArray* records) {
resolve(records);
} failure:^(NSError* error) { // ← typed as NSError*
reject(@"sync_error", error.localizedDescription, error); // ← line 401, the crash
}];
}
The failure block is typed ^(NSError* error), but TSLocationManager.sync actually passes an NSString* in some failure paths (likely when the error originates from the HTTP/Foundation layer rather than a structured NSError).
When reject is called with the NSString masquerading as NSError*:
RCTJSErrorFromCodeMessageAndNSError (React Native) calls [error code]
NSCFString does not respond to code
- →
unrecognized selector → crash
This is consistent with other RCT_EXPORT_METHODs in the same file that are correctly typed:
getLocations (line 391) → ^(NSString* error) ✅
getGeofences (line 411) → ^(NSString* error) ✅
addGeofence (line 437) → ^(NSString* error) ✅
So the typing of sync (and possibly setOdometer line 514, requestFullAccuracy line 669, getCurrentPosition line 339) as ^(NSError*) looks like a typing inconsistency on the Objective-C side.
Proposed Fix
Defensive wrap in the failure block that ensures reject always receives a proper NSError:
RCT_EXPORT_METHOD(sync:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
{
[locationManager sync:^(NSArray* records) {
resolve(records);
} failure:^(NSError* error) {
NSError *safeError = error;
if (![error isKindOfClass:[NSError class]]) {
// TSLocationManager sometimes passes NSString* instead of NSError* here.
// Wrap into a real NSError so RCTJSErrorFromCodeMessageAndNSError doesn't crash.
NSString *message = [error isKindOfClass:[NSString class]]
? (NSString *)error
: @"Unknown sync error";
safeError = [NSError errorWithDomain:@"RNBackgroundGeolocation"
code:-1
userInfo:@{NSLocalizedDescriptionKey: message}];
}
reject(@"sync_error", safeError.localizedDescription, safeError);
}];
}
Questions for the maintainer
-
Is this fix safe? From our analysis it doesn't change any documented behaviour:
- JS still receives a rejected promise with
code: 'sync_error' and a meaningful message
- Neither the SDK's own JS code nor consumer apps rely on the native
userInfo of the rejected error
- All it does is prevent the crash and let
try/catch work as expected
-
Is the underlying typing inconsistency in TSLocationManager intentional? I.e., are there cases where you want the failure callback to receive a non-NSError? Or should TSLocationManager.sync always pass NSError* and this is the real bug?
-
Should we apply the same defensive wrap to setOdometer (line 514), requestFullAccuracy (line 669), and getCurrentPosition (line 339)? They have the same ^(NSError* error) signature and could in theory exhibit the same issue.
-
We will apply a patch-package workaround on our side. Could you confirm whether this defensive wrap is the right approach, or whether you have a different fix in mind that we should wait for?
Number of users impacted
We see several reports with similar stack traces involving RCTJSErrorFromCodeMessageAndNSError + unrecognized selector".
Crash logs / sysdiagnose: just below.
Thank you for your time on this — happy to PR the fix if you're open to it.
Plugin Code and/or Config
{
activity = {
activityRecognitionInterval = 10000;
disableMotionActivityUpdates = 0;
disableStopDetection = 0;
minimumActivityRecognitionConfidence = 50;
stopDetectionDelay = 0;
stopOnStationary = 0;
triggerActivities = "";
};
activityRecognitionInterval = 10000;
activityType = 1;
app = {
heartbeatInterval = 60;
preventSuspend = 1;
schedule = (
);
startOnBoot = 1;
stopOnTerminate = 0;
};
authorization = {
accessToken = "";
expires = "-1";
refreshHeaders = {
Authorization = "Bearer {accessToken}";
};
refreshPayload = {
};
refreshToken = "";
refreshUrl = "";
strategy = JWT;
};
autoSync = 1;
autoSyncThreshold = 10;
batchSync = 1;
debug = 0;
desiredAccuracy = "-1";
didDeviceReboot = 0;
didLaunchInBackground = 0;
didRequestUpgradeLocationAuthorization = 0;
disableAutoSyncOnCellular = 0;
disableElasticity = 0;
disableLocationAuthorizationAlert = 1;
disableMotionActivityUpdates = 0;
disableStopDetection = 0;
distanceFilter = 10;
elasticityMultiplier = "1.2";
enableTimestampMeta = 0;
enabled = 1;
extras = {
installationId = "*************";
userId = "**************";
};
geofenceInitialTriggerEntry = 1;
geofenceProximityRadius = 2000;
geofenceTemplate = "";
geolocation = {
activityType = 1;
desiredAccuracy = "-1";
disableElasticity = 0;
disableLocationAuthorizationAlert = 1;
distanceFilter = 10;
elasticityMultiplier = "1.2";
enableTimestampMeta = 0;
filter = {
burstWindow = 10;
filterDebug = 0;
kalmanDebug = 0;
kalmanProfile = 0;
maxBurstDistance = 300;
maxImpliedSpeed = 60;
odometerAccuracyThreshold = 20;
odometerUseKalmanFilter = 1;
policy = 2;
rollingWindow = 5;
trackingAccuracyThreshold = 100;
useKalman = 1;
};
geofenceInitialTriggerEntry = 1;
geofenceProximityRadius = 2000;
locationAuthorizationAlert = {
cancelButton = Cancel;
instructions = "To use background location, you must enable '{locationAuthorizationRequest}' in the Location Services settings";
settingsButton = Settings;
titleWhenNotEnabled = "Background location is not enabled";
titleWhenOff = "Location services are off";
};
locationAuthorizationRequest = Always;
locationTimeout = 60;
pausesLocationUpdatesAutomatically = 1;
showsBackgroundLocationIndicator = 1;
stationaryRadius = 25;
stopAfterElapsedMinutes = "-1";
stopTimeout = 5;
useSignificantChangesOnly = 0;
};
headers = {
"X-App-Key" = ************;
};
heartbeatInterval = 60;
http = {
autoSync = 1;
autoSyncThreshold = 10;
batchSync = 1;
disableAutoSyncOnCellular = 0;
headers = {
"X-App-Key" = ************;
};
maxBatchSize = 50;
method = POST;
params = {
};
rootProperty = locations;
timeout = 60000;
url = "http://192.168.1.118:3333/v1/locations";
};
httpRootProperty = locations;
httpTimeout = 60000;
iOSHasWarnedLocationServicesOff = 0;
includeDeprecatedPropertiesInDictionary = 1;
isFirstBoot = 0;
isMoving = 0;
lastLocationAuthorizationStatus = 0;
locationAuthorizationAlert = {
cancelButton = Cancel;
instructions = "To use background location, you must enable '{locationAuthorizationRequest}' in the Location Services settings";
settingsButton = Settings;
titleWhenNotEnabled = "Background location is not enabled";
titleWhenOff = "Location services are off";
};
locationAuthorizationRequest = Always;
locationTemplate = "";
locationTimeout = 60;
locationsOrderDirection = ASC;
logLevel = 5;
logMaxDays = 3;
logger = {
debug = 0;
logLevel = 5;
logMaxDays = 3;
};
maxBatchSize = 50;
maxDaysToPersist = 60;
maxRecordsToPersist = "-1";
method = POST;
minimumActivityRecognitionConfidence = 50;
odometer = "0.0002807877272674074";
odometerError = "27.53921569316209";
params = {
};
pausesLocationUpdatesAutomatically = 1;
persistMode = 2;
persistence = {
extras = {
installationId = "*************";
userId = "**************";
};
geofenceTemplate = "";
locationTemplate = "";
locationsOrderDirection = ASC;
maxDaysToPersist = 60;
maxRecordsToPersist = "-1";
persistMode = 2;
timestampFormat = iso;
};
preventSuspend = 1;
schedule = (
);
schedulerEnabled = 0;
showsBackgroundLocationIndicator = 1;
startOnBoot = 1;
stationaryRadius = 25;
stopAfterElapsedMinutes = "-1";
stopDetectionDelay = 0;
stopOnStationary = 0;
stopOnTerminate = 0;
stopTimeout = 5;
timestampFormat = iso;
trackingMode = 1;
triggerActivities = "";
url = "http://192.168.1.118:3333/v1/locations";
useSignificantChangesOnly = 0;
}
Relevant log output
On Xcode, from `RNBackgroundGeolocation.mm`
RCT_EXPORT_METHOD(sync:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
{
[locationManager sync:^(NSArray* records) {
resolve(records);
} failure:^(NSError* error) {
reject(@"sync_error", error.localizedDescription, error);
}];
}
Required Reading
Plugin Version
5.1.1
Mobile operating-system(s)
Device Manufacturer(s) and Model(s)
iPhone 15 & iPhone 14 Plus
Device operating-systems(s)
iOS 26.3.1
React Native / Expo version
"react-native": "0.81.5", "expo": "~54.0.32",
What happened?
Your Environment
5.1.1react-native --version): 0.76+autoSync: true, custom HTTP endpoint viaurl+headersExpected Behaviour
When
BackgroundGeolocation.sync()fails (HTTP timeout, server error, network drop), the JS promise should reject with a normalErrorobject that can be caught withtry/catch.Actual Behaviour
The app hard-crashes natively (process killed by iOS) with the following exception:
Stack trace (relevant frames):
The JS-side
try/catchcannot catch this — the crash happens in the nativerejectcallback before the promise reaches the JS bridge.Steps to Reproduce
Hard to reproduce on demand, but the pattern is:
BackgroundGeolocation.sync()is called (manually or viaautoSync)TSHttpService) fails for any reason (timeout, 5xx, connection drop, slow Wi-Fi)We have 3 reproducible TestFlight feedbacks from the same user on iOS 26.3.1 over the past 10 days, and a clean Xcode crash report attached below.
Root Cause Analysis
Looking at
RNBackgroundGeolocation.mmline 396–403:The
failureblock is typed^(NSError* error), butTSLocationManager.syncactually passes anNSString*in some failure paths (likely when the error originates from the HTTP/Foundation layer rather than a structuredNSError).When
rejectis called with theNSStringmasquerading asNSError*:RCTJSErrorFromCodeMessageAndNSError(React Native) calls[error code]NSCFStringdoes not respond tocodeunrecognized selector→ crashThis is consistent with other
RCT_EXPORT_METHODs in the same file that are correctly typed:getLocations(line 391) →^(NSString* error)✅getGeofences(line 411) →^(NSString* error)✅addGeofence(line 437) →^(NSString* error)✅So the typing of
sync(and possiblysetOdometerline 514,requestFullAccuracyline 669,getCurrentPositionline 339) as^(NSError*)looks like a typing inconsistency on the Objective-C side.Proposed Fix
Defensive wrap in the failure block that ensures
rejectalways receives a properNSError:Questions for the maintainer
Is this fix safe? From our analysis it doesn't change any documented behaviour:
code: 'sync_error'and a meaningfulmessageuserInfoof the rejected errortry/catchwork as expectedIs the underlying typing inconsistency in
TSLocationManagerintentional? I.e., are there cases where you want thefailurecallback to receive a non-NSError? Or shouldTSLocationManager.syncalways passNSError*and this is the real bug?Should we apply the same defensive wrap to
setOdometer(line 514),requestFullAccuracy(line 669), andgetCurrentPosition(line 339)? They have the same^(NSError* error)signature and could in theory exhibit the same issue.We will apply a
patch-packageworkaround on our side. Could you confirm whether this defensive wrap is the right approach, or whether you have a different fix in mind that we should wait for?Number of users impacted
We see several reports with similar stack traces involving RCTJSErrorFromCodeMessageAndNSError + unrecognized selector".
Crash logs / sysdiagnose: just below.
Thank you for your time on this — happy to PR the fix if you're open to it.
Plugin Code and/or Config
Relevant log output