Why upgrades hurt more the longer you wait
An Expo SDK version bundles a specific React Native version, a set of Expo modules and a build toolchain that all expect each other. When a new SDK lands, the gap between yours and current is small and the migration notes are short. Wait a year and you are not doing one upgrade, you are doing four, and every breaking change in every one of them arrives simultaneously in a single error log.
The forcing function is usually external. Apple periodically raises the minimum SDK requirements for submissions, and older Expo versions eventually stop being supported for builds. That means the upgrade you postponed becomes the upgrade you have to complete under deadline, with a bug fix waiting behind it. Doing it on your own schedule, one version at a time, is dramatically cheaper.
Before you change anything
Half of the pain in a bad upgrade comes from not being able to tell what you broke versus what was already broken. Spend fifteen minutes establishing a clean starting point and the whole job gets easier.
- Commit everything and start from a clean working tree. An upgrade touches a lot of files and you need the diff to be readable.
- Create a branch for the upgrade. This is the single most important step, because abandoning a bad upgrade should be one command rather than an afternoon of undoing.
- Confirm the app currently builds and runs. If it does not, fix that first. Upgrading on top of an existing break is how people lose a day.
- Write down which SDK version you are on and which one you are moving to, and read the migration notes for that exact hop.
- Note any native or config-plugin dependencies you rely on. These are the packages most likely to lag behind a new SDK, and knowing them in advance tells you whether to wait.
The upgrade itself, one version at a time
The mechanics are short. The discipline is in doing them in order and stopping to verify between steps rather than running everything and looking at the wreckage.
The upgrade sequence
| Step | What you are doing | Why this order |
|---|---|---|
| Bump the SDK | Install the target SDK version for the next release, not the latest | One hop at a time keeps breaking changes attributable |
| Align dependencies | Run Expo's dependency check and fix the versions it flags | Mismatched packages produce misleading errors in every later step |
| Clear caches | Remove node modules, lockfile artefacts and the bundler cache, then reinstall | Stale caches cause failures that look like real code problems |
| Run in development | Get it launching on a simulator or device in dev mode | Fastest feedback loop; catches most issues in seconds not minutes |
| Fix the code | Work through deprecations and renamed APIs from the migration notes | Now you have a running app to test each fix against |
| Build for real | Produce an actual development build, then a production one | Native-layer problems only surface here, so do it last |
Commit after each step that succeeds. A dozen small commits on the upgrade branch means that when step five explodes you go back to step four rather than to the beginning. If you are unfamiliar with the build side of this, [the EAS build guide for Expo apps](/blog/eas-build-guide-for-expo-apps) covers producing the development and production builds you will need at the end.
Where upgrades usually break
Across a lot of upgrades the same handful of things account for most of the lost time, and all of them are recognisable once you have seen them.
- A third-party package that has not been updated for the new SDK. Check its repository for an issue about the version you are targeting before you start debugging your own code.
- Renamed or split Expo modules. Functionality regularly moves between packages between versions, and the error usually reads as a missing module rather than a moved one. The migration notes name these explicitly.
- Config plugin changes. Anything that modifies the native project can fail quietly in development and loudly at build time, which is why the real build comes last.
- Cached state pretending to be a code bug. If an error makes no sense, clear everything and reinstall before investigating further. This resolves a surprising share of them.
- Navigation and router API changes. If you use Expo Router, its API moves alongside the SDK. [How to use Expo Router for app navigation](/blog/how-to-use-expo-router-for-app-navigation) covers the current patterns.
- Permission and privacy declarations shifting. New SDKs sometimes change how native permissions are declared, which surfaces at App Store submission rather than at build. [How to fix the privacy manifest error in your Expo app](/blog/how-to-fix-the-privacy-manifest-error-in-your-expo-app) covers that class of problem.
Testing before you ship it
An upgrade that runs in development is not an upgrade that works. The changes underneath touch the native layer, and the failures that matter show up on real hardware in situations the simulator does not reproduce. Test these specifically.
- Cold start on a real device, both platforms. Simulator launches hide a category of startup problems entirely.
- Anything touching the camera, notifications, location or storage. Native permission flows are the most common casualty of an SDK bump.
- Offline behaviour and reconnection. Networking layers change between versions more often than people expect.
- In-app purchases in sandbox if you have them, because payment plumbing is native and unforgiving. [How to test in-app purchases in sandbox mode](/blog/how-to-test-in-app-purchases-in-sandbox-mode) covers the setup.
- An upgrade path from the currently shipped version, not a fresh install. Existing users have stored data written by the old version, and a migration that only ever gets tested on clean installs is a migration that has not been tested.
- Push a build to your testers before production. [The TestFlight beta testing guide for Expo apps](/blog/testflight-beta-testing-guide-for-expo-apps) is the safety net for exactly this.
Making the next one boring
The goal is for SDK upgrades to become a scheduled chore rather than an emergency. Three habits get you there.
- Upgrade on a cadence rather than on a crisis. Put it in the calendar at a fixed interval and do it whether or not anything is forcing you.
- Keep a short upgrade log in the repository. Which version, what broke, what fixed it. Future you will hit the same package problem and the note saves an hour.
- Prefer packages that track Expo releases closely. When choosing between two libraries, the one that ships an update within weeks of each SDK is worth more than a marginally better API.
Crash reporting is what tells you whether an upgrade actually went cleanly for real users rather than just for you. If you do not have it wired up, [how to add crash reporting to your Expo app](/blog/how-to-add-crash-reporting-to-your-expo-app) is worth doing before the next upgrade rather than after.
Short, practical drops on app ideas, validating fast, store listings, and passing app review. No spam, unsubscribe anytime.
Frequently asked questions
Can I skip Expo SDK versions when upgrading?
Technically sometimes, practically no. Each version has its own migration notes and its own breaking changes, and jumping several at once means every one of those changes lands in the same error log with no way to attribute which caused what. One hop at a time, committing after each, turns a multi-day debugging session into a sequence of short, understandable steps.
How often should I upgrade my Expo SDK?
On a schedule you choose rather than when something forces you. Staying within a version or two of current keeps each upgrade small, and it means that when Apple raises submission requirements or you need to ship an urgent fix, you are not blocked behind a large migration. The exact cadence depends on your release rhythm, but the principle is that regular small upgrades cost far less in total than occasional large ones.
What do I do if a package I depend on does not support the new SDK?
Check the package's repository first for an open issue or a pre-release that targets the new version, because someone has usually already asked. If there is nothing, your options are to wait, to find a maintained alternative, or to fork it - and waiting is almost always right unless the upgrade is forced. This is exactly why you check your native dependencies before starting rather than discovering it mid-upgrade.
Why does my app work in development but fail after upgrading and building?
Because development mode does not exercise the native layer the same way a real build does. Config plugin changes, native permission declarations and packages with native code can all pass in development and fail at build time. That is why the sequence puts the real build last and why you should never treat a successful development run as proof the upgrade is finished.
Should I upgrade the Expo SDK before or after submitting an app update?
After, unless the upgrade is what the submission requires. Never bundle an SDK upgrade with a feature release you are under pressure to ship, because if the upgrade goes badly you cannot separate the two and your fix is stuck behind a migration. Ship the feature, then upgrade on its own branch with time to test it properly.
Last reviewed by David on August 19, 2026


