An Expo app on the appVersion runtimeVersion policy adds a native camera module, the team publishes an EAS Update without bumping version, and 1.4.0 users crash at launch: what went wrong, and how do you prevent it?
answer
- what did the label say?
- exact match, nobody checks imports
- missing native half at import time
- new build, new runtime, release branch
basics
~20 sappVersion still resolved to 1.4.0, so EAS served the update to 1.4.0 binaries that lack the camera's native code, and its import threw at launch. Ship native changes in a new build with a new runtime version.
solid answer
~40 sWith `appVersion`, `eas update` computed the runtime version from the unchanged `version`, still `1.4.0`, so EAS matched the update to every 1.4.0 binary. The bundle imports `expo-camera`, whose JavaScript calls `requireNativeModule('ExpoCamera')`; the binary has no such module, so it throws `Cannot find native module` as soon as that import is evaluated, here at startup. First, serve the last good bundle to runtime 1.4.0 again. Then ship the feature properly: bump to 1.5.0, build and submit, and publish the camera update for runtime 1.5.0 only, while 1.4.0 hotfixes come from a release branch without the camera code. To prevent a repeat, adopt the `fingerprint` policy or a rule that native changes bump `version`, check the resolved runtime in CI, and roll out gradually.
code
bash · 6 lines# Runtime versions this checkout would publish to (JSON with a runtimeVersion field)
npx expo-updates runtimeversion:resolve --platform ios
npx expo-updates runtimeversion:resolve --platform android
# What changed in the native layer since the build that is in the store
eas fingerprint:compare --build-id "$STORE_BUILD_ID"go deeper
Recall that an update cannot add native code, and that the runtime version label is what lets old builds accept an update.
Explain why appVersion produced the same runtime, and why the missing native module throws during import rather than later.
Walk through stopping the damage, shipping the feature on a new runtime, keeping a release branch for the old one, and the CI and rollout checks that prevent a repeat.
Decide which safeguard the organisation standardises on, a policy that tracks native changes or a human bump rule backed by CI, and who is accountable when it fails.
## What the team shipped A typical timeline for this failure: 1. Version `1.4.0` of the app is built and released to the stores. Its app config has `"version": "1.4.0"` and `"runtimeVersion": { "policy": "appVersion" }`, so every 1.4.0 binary reports runtime version `1.4.0`. 2. A developer adds `expo-camera` for a new scanning screen. The library has a JavaScript half and a native half. 3. The screen is finished, and the team runs `eas update` from the main branch. Nobody changes `version`. 4. Over the following app launches, crash reports arrive from 1.4.0 users on both platforms, all at launch. ## Why EAS delivered the update With the `appVersion` policy, `eas update` resolved the runtime version from the unchanged `version` field: still `1.4.0`. EAS Update matches updates to builds by **exact** runtime version and platform, so every 1.4.0 build accepted the update. Nothing in the pipeline inspected which native modules the bundle imports; the runtime version is a label the project asserts, and here the label was wrong. ## Why it crashes at launch The JavaScript half of an Expo module looks up its native half when it is imported. `expo-camera` calls `requireNativeModule('ExpoCamera')`, and when the binary has no such module that call throws `Cannot find native module 'ExpoCamera'`. When the screen that imports it is loaded at startup, as it is when the import sits at module scope in a file the app evaluates on launch, the error fires before the first screen renders. `expo-updates` has error recovery that may mark a freshly launched update as failed and fall back to an older update when the error happens before the first view appears. Treat it as a last resort: users may still see a crash, and an error thrown after content has appeared is not rolled back. ## Stopping the damage and the real fix - **Stop the damage:** serve the last good bundle to runtime `1.4.0` again, by republishing the previous update or rolling back. Those commands have their own mechanics; the point here is that the fix is published to the same runtime version that is broken. - **Ship the feature properly:** bump `version` to `1.5.0`, make and submit new builds (runtime `1.5.0`), and publish the camera screen as an update for that runtime only. - **Keep 1.4.0 serviceable:** once main contains the camera code, any update built from main is wrong for 1.4.0. Hotfixes for 1.4.0 must come from a branch cut at the 1.4.0 release, which still resolves to runtime `1.4.0`. ## Diagnosing it with evidence | Evidence | What it tells you | |---|---| | `Updates.runtimeVersion` and `Updates.updateId` logged with the crash | Which native layer and which update were running | | The crash message `Cannot find native module '...'` | A JS-to-native mismatch, not a logic bug | | `eas fingerprint:compare --build-id <build> --update-id <update>` | The native inputs that differ between the store build and the update, such as a new autolinked package | ## Preventing a repeat 1. **Make the label follow native changes.** Either adopt a rule that any native change bumps `version`, or switch to `{ "policy": "fingerprint" }`, which changes the runtime version automatically. With fingerprint the same mistake produces an update that no installed build matches: nobody receives it, so no installed build runs it. 2. **Check before publishing.** In CI, run `npx expo-updates runtimeversion:resolve --platform ios` (and `android`) to print the runtime version the publish would target, and compare the project with the last store build using `eas fingerprint:compare`. 3. **Limit the blast radius.** Publish to a small percentage first and test on a preview build that shares the production runtime. 4. **Mind JS guards.** `requireOptionalNativeModule` from `expo` returns `null` instead of throwing, so it can hide a feature whose native half is missing, but only if nothing imports the library eagerly. It is a mitigation, not a substitute for a correct runtime version. ## Why the other policies would not have saved it - `nativeVersion` resolves from `version` and the build number. Publishing without a new build changes neither, so the update would have carried the old runtime just the same. - A hand-managed string fails the same way unless someone edits it. - Only `fingerprint` recomputes from the native inputs themselves, which is why it turns this crash into an update that nobody receives. ## The lesson Every policy other than `fingerprint` is a promise made by people. `appVersion` and `nativeVersion` are only as correct as the discipline of bumping numbers when native code changes; a manual string is only as correct as the person editing it.
- Would the fingerprint policy have prevented this crash?Yes, in the sense that matters: adding `expo-camera` changes the autolinked native sources, so the publish would have produced a new hash that no installed build reports. The update would reach nobody instead of crashing everyone. The feature would still need a new build before any user could get it.
- Can a JavaScript guard make the new screen safe on old builds?Partly. `requireOptionalNativeModule` from `expo` returns `null` instead of throwing, so code can hide a feature whose native half is missing. It only helps if nothing imports the library eagerly at module scope, because the library's own import would still throw. It is a mitigation, not a substitute for a correct runtime version.
- Why must 1.4.0 hotfixes come from a release branch after this?Because main now contains code that imports the camera module. Any bundle built from main still resolves to runtime 1.4.0 under appVersion but depends on native code 1.4.0 lacks. Cutting fixes from the commit that produced 1.4.0 keeps the update layer consistent with that binary's native layer.
saying these in an interview costs you the question
- EAS Update rejects an update whose JavaScript imports a native module the build lacks.
- expo-updates error recovery means users never see a crash from a bad update.
- Changing the runtime version policy fixes the crash for existing 1.4.0 installs.
- The native module is downloaded along with the update the first time it is needed.
- After the fix is on main, 1.4.0 hotfixes can still be published from main.