Summary
Production crash reported by Google Play vitals: applying initialRegion at map-ready time crashes inside the Google Maps SDK with its obfuscated apiexception — the documented "Error using newLatLngBounds(LatLngBounds, int): Map size can't be 0. Most likely, layout has not yet occurred for the map view." error.
The crash path is onMapReady → applyBridgedProps() → moveToRegion() → map.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, 0)) — i.e. MapView.java:534 → 826 → 881 in 1.27.2 (line numbers in the stack below match the released sources exactly).
All events so far come from one device: Pixel 9 Pro XL on Android 17 (API 37), repeating (9 events, same user), with the current Play-services-delivered maps renderer (policy_maps_core_dynamite@260830210). It looks like the newest OS/renderer combination has shifted the onMapReady-vs-internal-layout timing so the long-known race now fires consistently on that hardware. Expect more reports as Android 17 rolls out.
Stack trace
com.google.maps.api.android.lib6.common.apiexception.b:
at com.google.maps.api.android.lib6.common.v.a (:com.google.android.gms.policy_maps_core_dynamite@260830210@260830203025.947725257.947725257:15)
at com.google.maps.api.android.lib6.phoenix.camera.t.j (...:123)
at com.google.maps.api.android.lib6.impl.ag.a (...:10)
at com.google.maps.api.android.lib6.phoenix.camera.t.s (...:60)
at com.google.maps.api.android.lib6.impl.cg.v (...:26)
at com.google.android.gms.maps.internal.j.br (...:386)
at m140.azc.onTransact (...:21)
at android.os.Binder.transact (Binder.java:1230)
at com.google.android.gms.internal.maps.zza.zzc (com.google.android.gms:play-services-maps@@19.1.0:2)
at com.google.android.gms.maps.internal.zzg.moveCamera (com.google.android.gms:play-services-maps@@19.1.0:3)
at com.google.android.gms.maps.GoogleMap.moveCamera (com.google.android.gms:play-services-maps@@19.1.0:2)
at com.rnmaps.maps.MapView.moveToRegion (MapView.java:881)
at com.rnmaps.maps.MapView.applyBridgedProps (MapView.java:826)
at com.rnmaps.maps.MapView.onMapReady (MapView.java:534)
at com.google.android.gms.maps.zzag.zzb (com.google.android.gms:play-services-maps@@19.1.0:1)
at com.google.android.gms.maps.internal.zzas.zza (com.google.android.gms:play-services-maps@@19.1.0:5)
at com.google.android.gms.internal.maps.zzb.onTransact (com.google.android.gms:play-services-maps@@19.1.0:3)
at android.os.Binder.transact (Binder.java:1230)
...
at android.os.Looper.loop (Looper.java:397)
at android.app.ActivityThread.main (ActivityThread.java:9523)
Analysis
moveToRegion guards against the pre-layout case:
if (super.getHeight() <= 0 || super.getWidth() <= 0) {
map.moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(lat, lng), 10));
boundsToMove = bounds;
} else {
map.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, 0)); // <-- line 881, throws
boundsToMove = null;
}
but the guard checks the outer RN view's size. The Maps SDK's camera keeps its own internal viewport size, which is registered asynchronously after the SDK attaches its internal view hierarchy (which itself happens asynchronously once the dynamite module loads). When onMapReady is delivered before that internal size registration completes, newLatLngBounds(LatLngBounds, int) — the only size-dependent CameraUpdate — throws, and since onMapReady runs on the main looper the app dies. So the crash fires exactly when the outer view is already laid out (guard passes, else-branch taken) but the internal size is still 0.
The window is normally microscopic, which is why this has been a rare-but-persistent crash across every Google Maps wrapper for years (same class as flutter/flutter#135874, and the ancient #245 / #3154 here). What's new is that on Android 17 with the current renderer it reproduces persistently on at least one mainstream device.
Two amplifying observations:
- In our app the map most exposed is a 1×1, offscreen, alpha-0 MapView used as an engine warm-up. Its outer size (1×1) always passes the
> 0 guard, so it runs the vulnerable branch on every mount, and a never-drawn view plausibly has a much wider internal-size-registration window. A tiny/offscreen <MapView initialRegion={...}/> on an Android 17 device may be a good reproduction setup.
animateToRegion (MapView.java:1395) calls newLatLngBounds(bounds, 0) with no size guard at all, so any animateToRegion issued around mount time has the same exposure.
Suggested fix
Mirror the intent of the existing safe branch: wrap the size-dependent call and fall back to the deferred path, which updateExtraData already knows how to re-apply with explicit dimensions (newLatLngBounds(bounds, width, height, 0)) on the next layout:
} else {
try {
map.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, 0));
boundsToMove = null;
} catch (RuntimeException e) {
// Internal map size not registered yet ("Map size can't be 0") — defer to
// updateExtraData, which re-applies with explicit dimensions on next layout.
map.moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(lat, lng), 10));
boundsToMove = bounds;
}
}
(catch (RuntimeException) because the concrete exception class is an obfuscated internal type of the dynamite module and can't be referenced.) The same treatment would fit animateToRegion and the fitTo* methods.
Happy to open a PR with the above if maintainers agree with the approach.
Workaround for other users
On Android, use the camera forms instead of the region forms — CameraUpdateFactory.newCameraPosition has no layout/size restriction:
initialCamera instead of initialRegion (on Fabric it is even applied via GoogleMapOptions at map construction, so no post-ready camera move happens at all);
animateCamera instead of animateToRegion for moves that can occur near mount.
Note when constructing camera objects for Android: always include pitch and heading — cameraPositionFromMap reads them with ungated getDouble, so omitting them throws NoSuchKeyException.
Environment
- react-native-maps: 1.27.2 (crash line numbers match this release exactly); the unguarded call is still present on current
master
- react-native: 0.86.0 (New Architecture / Fabric), Expo SDK 57
- react: 19.2.3
- play-services-maps: 19.1.0 (the library default)
- Maps renderer (Play-services delivered):
com.google.android.gms.policy_maps_core_dynamite@260830210
- Device: Google Pixel 9 Pro XL, Android 17 (API 37) — 9 crash events, single device, repeating; no occurrences observed on older Android versions in the same app population
- Map: default provider (Google),
initialRegion set, no region/camera props
Summary
Production crash reported by Google Play vitals: applying
initialRegionat map-ready time crashes inside the Google Maps SDK with its obfuscatedapiexception— the documented "Error using newLatLngBounds(LatLngBounds, int): Map size can't be 0. Most likely, layout has not yet occurred for the map view." error.The crash path is
onMapReady → applyBridgedProps() → moveToRegion() → map.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, 0))— i.e.MapView.java:534 → 826 → 881in 1.27.2 (line numbers in the stack below match the released sources exactly).All events so far come from one device: Pixel 9 Pro XL on Android 17 (API 37), repeating (9 events, same user), with the current Play-services-delivered maps renderer (
policy_maps_core_dynamite@260830210). It looks like the newest OS/renderer combination has shifted theonMapReady-vs-internal-layout timing so the long-known race now fires consistently on that hardware. Expect more reports as Android 17 rolls out.Stack trace
Analysis
moveToRegionguards against the pre-layout case:but the guard checks the outer RN view's size. The Maps SDK's camera keeps its own internal viewport size, which is registered asynchronously after the SDK attaches its internal view hierarchy (which itself happens asynchronously once the dynamite module loads). When
onMapReadyis delivered before that internal size registration completes,newLatLngBounds(LatLngBounds, int)— the only size-dependentCameraUpdate— throws, and sinceonMapReadyruns on the main looper the app dies. So the crash fires exactly when the outer view is already laid out (guard passes, else-branch taken) but the internal size is still 0.The window is normally microscopic, which is why this has been a rare-but-persistent crash across every Google Maps wrapper for years (same class as flutter/flutter#135874, and the ancient #245 / #3154 here). What's new is that on Android 17 with the current renderer it reproduces persistently on at least one mainstream device.
Two amplifying observations:
> 0guard, so it runs the vulnerable branch on every mount, and a never-drawn view plausibly has a much wider internal-size-registration window. A tiny/offscreen<MapView initialRegion={...}/>on an Android 17 device may be a good reproduction setup.animateToRegion(MapView.java:1395) callsnewLatLngBounds(bounds, 0)with no size guard at all, so anyanimateToRegionissued around mount time has the same exposure.Suggested fix
Mirror the intent of the existing safe branch: wrap the size-dependent call and fall back to the deferred path, which
updateExtraDataalready knows how to re-apply with explicit dimensions (newLatLngBounds(bounds, width, height, 0)) on the next layout:(
catch (RuntimeException)because the concrete exception class is an obfuscated internal type of the dynamite module and can't be referenced.) The same treatment would fitanimateToRegionand thefitTo*methods.Happy to open a PR with the above if maintainers agree with the approach.
Workaround for other users
On Android, use the camera forms instead of the region forms —
CameraUpdateFactory.newCameraPositionhas no layout/size restriction:initialCamerainstead ofinitialRegion(on Fabric it is even applied viaGoogleMapOptionsat map construction, so no post-ready camera move happens at all);animateCamerainstead ofanimateToRegionfor moves that can occur near mount.Note when constructing camera objects for Android: always include
pitchandheading—cameraPositionFromMapreads them with ungatedgetDouble, so omitting them throwsNoSuchKeyException.Environment
mastercom.google.android.gms.policy_maps_core_dynamite@260830210initialRegionset, noregion/cameraprops