expo: Updates.reloadAsync() crashes if called during first startup

Minimal reproducible example

https://apps.apple.com/tt/app/rideswup/id1573481198

Summary

Just noticed this behaviour now. I use reloadAsync to reload the app on account removal / logout, to make sure there’s nothing left in the app’s reducers. It seems to crash only the first time app is launched. Secondary startups of the same app with no actions taken in between, don’t crash.

It’s quite hard to give a reproducible example since this only happens in the production builds. You can however just download one of our apps here, and you can clearly see the app crashing when you try logging out for the first time, after logging in (during the first launch, but not on subsequent app launches).

Android does not seem to crash, so … not sure what’s causing it in the background, but the code is literally the same in between the two platforms.

So, it is obvious something happens on iOS, with the reloadAsync method on the first launch. I do see someone “solving” the issue by disabling Hermes on iOS only (even though user states issue is present on iPads only, even though, I guess it’s literally the same build (iOS, not iPadOS) just with tablet support).

LATER EDIT Upon further checking, I can say that the crash does not happen (on initial app launch) if you call it as follows (while you have an update available:

  1. checkForUpdateAsync
  2. fetchUpdateAsync
  3. reloadAsync

However:

  1. Calling it directly, outside the context of applying an update, it does crash.
  2. Also calling it, after applying an update and triggering reloadAsync, app reloads fine. If you however call it (just like before, directly, outside the context of applying a proper update, it does crash.

So, it seems it does not matter whether the app was previously reloaded properly with an update, as long as it’s the first app launch. Restarting the app once then calling reloadAsync, does NOT fail.

You can however see that there are quite a few people reporting the same (or very similar issues), after a quick search.

https://github.com/expo/expo/issues/19812 https://github.com/expo/expo/issues/17702 https://stackoverflow.com/questions/71600883/expo-update-reloadasync-crashes-app-in-release-build https://stackoverflow.com/questions/70785635/expo-ios-app-is-crashing-when-fetching-for-new-version https://forums.expo.dev/t/eas-update-expo-updates/62708 https://forums.expo.dev/t/upgrading-from-sdk45-to-sdk47-makes-updates-reloadasync-crash-app/69343 https://forums.expo.dev/t/updates-reloadasync-crashes-first-time-then-work-but-update-is-not-applied/69438 https://forums.expo.dev/t/reloadasync-crashes-app-in-release-build/62643 https://forums.expo.dev/t/out-of-memory-crashes-freezing-on-programmatic-ota-updates/65718 https://forums.expo.dev/t/expo-updates-crashes-the-app-on-ios/51472

Please advise

Environment

expo-env-info 1.0.5 environment info:
  System:
    OS: macOS 13.2.1
    Shell: 5.8.1 - /bin/zsh
  Binaries:
    Node: 16.18.1 - ~/.nvm/versions/node/v16.18.1/bin/node
    npm: 8.19.2 - ~/.nvm/versions/node/v16.18.1/bin/npm
  Managers:
    CocoaPods: 1.11.3 - /opt/homebrew/bin/pod
  SDKs:
    iOS SDK:
      Platforms: DriverKit 22.2, iOS 16.2, macOS 13.1, tvOS 16.1, watchOS 9.1
  IDEs:
    Xcode: 14.2/14C18 - /usr/bin/xcodebuild
  npmPackages:
    expo: ^47.0.6 => 47.0.13 
    react: 18.1.0 => 18.1.0 
    react-dom: 18.1.0 => 18.1.0 
    react-native: 0.70.5 => 0.70.5 
  npmGlobalPackages:
    eas-cli: 3.6.0
    expo-cli: 6.3.1
  Expo Workflow: managed

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Reactions: 3
  • Comments: 55 (18 by maintainers)

Most upvoted comments

@edi @Inspiravetion - I will be investigating this and attempting to reproduce in-house. In the meantime, the comment above by @brentvatne is valid. If you do have a small project that does not have a large number of dependencies, and reproduces the issue, it would be really helpful. I do appreciate all the details that you have shared so far (e.g. the steps described in https://github.com/expo/expo/issues/21347#issuecomment-1441009063)

I am still seeing this issue on Expo 50 on Android running Hermes. The call to Updates.checkForUpdateAsync() is hanging.

I can confirm the same issue, after reloadAsync if you call checkForUpdateAsync it doesn’t resolve or rejects. The problem with timeout @webdevmario @ilyausorov is that looks like in different mobiles happens at different times, so 3 seconds won’t always work. What I did is simply create a timeout around checkForUpdateAsync and you can retry if it didn’t finish or just ignore:

try {
  let timeout;
  const timeoutPromise = new Promise((_, reject) => {
    timeout = setTimeout(() => {
      reject(new Error('Promise timed out'));
    }, 3000);
  });
  update = (await Promise.race([
    timeoutPromise,
    Updates.checkForUpdateAsync(),
  ])) as Updates.UpdateCheckResult;
  clearTimeout(timeout);
} catch (error) {
 // handle error or retry with a counter
}

I have to say that I tested the next versions of expo-updates coming, with useUpdates hooks and everything worked really well without workarounds. It should come with Expo sdk 50 so shouldn’t take a long time.

Hi!..any update on this?

Hi. As a workaround, you can either switch to Hermes instead of JSC engine, and if that’s already the case, you can upgrade to SDK 48, which does that for you anyway.

The issue doesn’t seem to exist in the new version, tested on all apps, small and big.

+100 to this…I have been seeing this ever since I upgraded from 45-47 almost two weeks ago and have gotten no response on the forums despite seeing more and more people reporting the same thing…for me the scenario is as follows:

  1. deploy a new update to a channel using eas update --branch $_ENVIRONMENT --non-interactive --message $_RELEASE_ID
  2. open the staging/prod app build pointed at that branch
  3. SplashScreen.preventAutoHideAsync(); is called in the global scope before the app is rendered and stores start to load
  4. appUpdateStore().init() runs (before the splash screen is unlocked)
       const updateRes = await Updates.checkForUpdateAsync()

        if (updateRes.isAvailable) {
            // store initialization is async so this makes sure the splashscreen isn't unlocked exposing an older version of the app + the user doesn't see a flash on reload
            runInAction(() => this.waitingForReload = true)
            await Updates.fetchUpdateAsync()
            await Updates.reloadAsync()
        }

5a. on iOS: app crashes at await Updates.reloadAsync() 5b. on Android: await Updates.fetchUpdateAsync() hangs indefinitely 6. opening the app after the crash/hang updates like normal (on the first try if the update finished downloading, otherwise it takes two open/closes for android)

For context: I am building on EAS with a paid account and had no issues around this before 47 or any other problems upgrading to 47.

eas.json

{
  "cli": {
    "version": "^3.5.0"
  },
  "build": {
    "prod": {
      "channel": "prod",
      "env": {
        "YARN_ENABLE_IMMUTABLE_INSTALLS": "false",
        "_ENVIRONMENT": "prod"
      },
      "expoCli": "6.2.1"
    },
    "staging": {
      "extends": "prod",
      "distribution": "internal",
      "channel": "staging",
      "env": {
        "_ENVIRONMENT": "staging"
      }
    },
    "staging-simulator": {
      "extends": "staging",
      "ios": {
        "simulator": true
      }
    },
    "dev": {
      "extends": "staging",
      "developmentClient": "true",
      "distribution": "internal",
      "channel": "dev",
      "env": {
        "_ENVIRONMENT": "dev"
      }
    }
  }
}

app.config.js

const config = {
	"expo": {
	  "name": appName(),
	  "slug": "patch",
	  "owner": "raheem-ai",
	  "version": VERSION,
	  "orientation": "portrait",
	  "icon": "./assets/patch_default_logo.png",
	  "scheme": scheme(),
	  "splash": {
		"image": "./assets/splash.png",
		"resizeMode": "cover",
		"backgroundColor": "#ffffff"
	  },
	  "plugins": [
	  	"sentry-expo",
		[
			"@config-plugins/react-native-branch",
			branchConfig()
		],
		[
			"expo-screen-orientation",
			{
			  "initialOrientation": "PORTRAIT_UP"
			}
		]
	  ],
	  "hooks": {
		"postPublish": [
		  {
			"file": "sentry-expo/upload-sourcemaps",
			"config": {
			  "organization": "raheem-org",
			  "project": "patch",
			  "authToken": SENTRY_AUTH_TOKEN
			}
		  }
		]
	  },
	  "updates": {
		"fallbackToCacheTimeout": 0
	  },
	  "assetBundlePatterns": [
		"**/*"
	  ],
	  "ios": {
		"supportsTablet": true,
		"infoPlist": {
		  "NSLocationAlwaysUsageDescription": "testing",
		  "UIBackgroundModes": [
			"location",
			"fetch",
			"remote-notification"
		  ]
		},
		"buildNumber": IOS_BUILD_NUMBER,
		"bundleIdentifier": appId(),
		"config": {
		  "googleMapsApiKey": GOOGLE_MAPS_KEY
		}
	  },
	  "android": {
		"icon": "./assets/patch_default_logo.png",
		"googleServicesFile": servicesJsonPath,
		"versionCode": ANDROID_VERSION_CODE,
		"adaptiveIcon": {
		  "foregroundImage": "./assets/patch_adaptive_logo_foreground.png",
		  "backgroundImage": "./assets/patch_adaptive_logo_background.png"
		},
		"package": appId(),
		"permissions": [],
		"config": {
		  "googleMaps": { 
			"apiKey": GOOGLE_MAPS_KEY
		  }
		}
	  },
	  "androidStatusBar": {
		"barStyle": "light-content",
		"translucent": true
	  },
	  "web": {
		"favicon": "./assets/favicon.png"
	  },
	  "extra": {
		"eas": {
			"projectId": "xx-redacted-xx"
		},
		"apiHost": apiHost,
		"sentryDSN": SENTRY_DSN,
		"appEnv": appEnv(),
		"backendEnv": backendEnv(),
		"linkBaseUrl": branchConfig().iosAppDomain,
		"termsOfServiceVersion": termsOfServiceVersion(),
		"termsOfServiceLink": termsOfServiceLink()
	  },
	  "runtimeVersion": {
		"policy": "nativeVersion"
	  },
	  "updates": {
		"url": "https://u.expo.dev/xx-redacted-xx"
	  }
	}
}

package.json

{
...
"dependencies": {
    "@config-plugins/react-native-branch": "^4.0.0",
    "@expo/config-plugins": "^5.0.2",
    "@expo/prebuild-config": "^5.0.5",
    "@googlemaps/google-maps-services-js": "3.1.16",
    "@react-native-async-storage/async-storage": "~1.17.3",
    "@react-native-community/netinfo": "9.3.5",
    "@react-navigation/native": "^6.0.6",
    "@react-navigation/stack": "^6.0.11",
    "@sentry/react-native": "4.9.0",
    "assert": "^2.0.0",
    "axios": "^0.21.1",
    "buffer": "^6.0.3",
    "dotenv": "^16.0.1",
    "events": "^3.3.0",
    "expo": "^47.0.0",
    "expo-application": "~5.0.1",
    "expo-constants": "~14.0.2",
    "expo-dev-client": "~2.0.1",
    "expo-device": "~5.0.0",
    "expo-linear-gradient": "~12.0.1",
    "expo-linking": "~3.3.0",
    "expo-location": "~15.0.1",
    "expo-modules-autolinking": "~1.0.0",
    "expo-notifications": "~0.17.0",
    "expo-screen-orientation": "~5.0.1",
    "expo-secure-store": "~12.0.0",
    "expo-splash-screen": "~0.17.5",
    "expo-status-bar": "~1.4.2",
    "expo-task-manager": "~11.0.1",
    "expo-updates": "~0.15.6",
    "inversify": "^5.1.1",
    "inversify-react": "^1.0.0",
    "jest": "^26.6.3",
    "jest-expo": "^47.0.1",
    "lodash": "^4.17.21",
    "mobx": "^6.3.2",
    "mobx-persist-store": "^1.0.3",
    "mobx-react": "^7.2.0",
    "module-alias": "^2.2.2",
    "moment": "^2.29.1",
    "parse-full-name": "^1.2.6",
    "react": "18.1.0",
    "react-dom": "18.1.0",
    "react-native": "0.70.5",
    "react-native-branch": "^5.4.0",
    "react-native-calendar-picker": "^7.1.2",
    "react-native-gesture-handler": "~2.8.0",
    "react-native-get-random-values": "~1.8.0",
    "react-native-maps": "1.3.2",
    "react-native-paper": "^4.9.2",
    "react-native-reanimated": "~2.12.0",
    "react-native-safe-area-context": "4.4.1",
    "react-native-screens": "~3.18.0",
    "react-native-scroll-into-view": "^2.0.2",
    "react-native-svg": "13.4.0",
    "react-native-vector-icons": "^8.1.0",
    "react-native-web": "~0.18.7",
    "react-native-wheel-picker-expo": "^0.4.2",
    "react-router-dom": "^5.2.0",
    "react-string-replace": "^1.1.0",
    "reflect-metadata": "^0.1.13",
    "sentry-expo": "~6.0.0",
    "socket.io-client": "^4.4.0",
    "socket.io-mock": "^1.3.2",
    "stream-browserify": "^3.0.0",
    "url": "^0.11.0",
    "uuid": "^8.3.2"
  }
...
}

@ewein & @billyhawkes I had this problem too (you can see me commenting on this issue some months ago). I haven’t had any problem with the new hooks API (I really think the old way is broken). Not your fault since the docs aren’t very clear about how to use it. Here’s my implementation and, hopefully, you make it work:

 const {isUpdateAvailable, isUpdatePending, isChecking} = Updates.useUpdates();
 const [updateCheckRequested, setUpdateCheckRequested] = useState(false);
 
 useEffect(() => {
   Updates.checkForUpdateAsync().catch((_error) => {});
 }, []);

useEffect(() => {
   if (isChecking && !updateCheckRequested) {
     setUpdateCheckRequested(true);
   }
 }, [isChecking, updateCheckRequested]);

 useEffect(() => {
   if (isUpdateAvailable) {
     Updates.fetchUpdateAsync().catch((_error) => {});
   } else if (isUpdateAvailable) {
       // You can also call this inside a modal with an "Update now" button for better UX
      Updates.fetchUpdateAsync().catch((_error) => {}
   }
 }, [isUpdateAvailable]);

 useEffect(() => {
   if (isUpdatePending) {
     Updates.reloadAsync().catch((_error) => {});
   }
 }, [isUpdatePending]);

Hello, I’m able to reproduce this crash on SDK 48 and SDK 49. My crash occurs when restarting the app after changing its language.

The crash happens when using the Updates.reloadAsync() function to restart the app. I have similar behaviour with the use of react-native-restart…

I created a blank project on GitHub to replicate the issue, but no crash occurred in this context, upon further analysis, I noticed that animations were active at the time of calling Updates.reloadAsync(), specifically a “bounce” animation from reanimated.

I reduced the duration of the animation to 250 ms and I added a delay before executing Updates.reloadAsync(). These changes prevented the crash in my tests.

I suspect that restarting the JavaScript bundle during an animation with reanimated might be causing the issue. I am not 100% certain that this solution definitively resolves the problem 🤷‍♂️

haha, you’re welcome! ¯_(ツ)_/¯ good luck making a minimally reproducible example for something like this. Sometimes you just gotta fix your own app and move on

Very interesting! Since the problem is fixed in SDK 48, I will create an engineering task to investigate this and see if there are any patches that we can safely backport to SDK 47 to fix this issue. Of course, we obviously prefer that you move to SDK 48 😄

I can confirm, it’s only SDK 47 with the issue. Just upgraded to SDK 48, made a new build, ran Updates.reloadAsync on the first app launch, didn’t crash.

Reproducing this is as easy as it gets. Make a blank new app, (SDK 47) install expo-updates, make a simulator build, trigger the reloadAsync method on the first app launch, it crashes. Re-open the app, trigger the same method, doesn’t crash.

You don’t need any pushed updates, nothing. Simply calling reloadAsync with the first launch of the app, causes it to crash, while on subsequent app launches, it doesn’t.

@douglowder @brentvatne here is the repo. It doesn’t get to call reloadAsync() because the call to checkForUpdateAsync() never returns that one is ready.

to test:

  1. yarn run build
  2. install app
  3. open to see screen color
  4. toggle commented out back ground colors
  5. yarn run update
  6. reopen app and see ‘No new update’ alert pop up
  7. close and reopen app to see the updated background color

@douglowder i will create a clonable repo so you can test, but basically, just thi (on mobile now):

  1. blank expo app (latest sdk, eg 47)
  2. Install and configure expo-updates
  3. Add onPress to the Text component which calls Updates.reloadAsync()
  4. Run eas build -p ios (basic production build)
  5. install the app through testflight or build it locally (I always use eas build so I cannot test via local build).
  6. Open app for the first time, and press the text element from step 3. App will crash.
  7. Open the app again, press the same element again, all works fine.