skip to content

An Expo app's push-notification feature fails in Expo Go on Android and only warns on iOS; why, and what do you build instead?

level: middleimportance: should knowfreq 45%

answer

  1. your push credentials, not Expo Go's
  2. Android remote push removed in SDK 53
  3. throws on Android, warns on iOS
  4. local notifications still work
  5. isRunningInExpoGo() to guard

basics

~20 s

Remote push depends on the app's own push credentials, which Expo Go cannot carry, and Android push was removed from Expo Go in SDK 53, so expo-notifications throws there. Build a development build with expo-dev-client and test push in it.

solid answer

~50 s

Remote push notifications are tied to the app's identity: its bundle identifier or package name and its own push credentials. Expo Go is a single shared app, so it cannot represent your app to the push services. On Android, Expo removed remote push from Expo Go in SDK 53, and in current SDKs `expo-notifications` **throws** when you ask Expo Go on Android for a push token (`getDevicePushTokenAsync`, which `getExpoPushTokenAsync` calls); on iOS it only logs a warning in development, and the feature should still be tested in your own build for parity with production. Local, scheduled notifications keep working in Expo Go. The fix is a development build: `npx expo install expo-dev-client`, then build with `npx expo run:android` / `run:ios` or on EAS, with your push credentials configured. Guard any Expo Go code path with `isRunningInExpoGo()`.

code

tsx · 13 lines
tsx
import { isRunningInExpoGo } from 'expo';
import * as Notifications from 'expo-notifications';

export async function registerForPush(): Promise<string | null> {
  if (isRunningInExpoGo()) {
    // Remote push needs a development build; skip in Expo Go.
    return null;
  }
  const { status } = await Notifications.requestPermissionsAsync();
  if (status !== 'granted') return null;
  const token = await Notifications.getExpoPushTokenAsync();
  return token.data;
}

go deeper

for a junior

Recall that remote push needs your own app build, not Expo Go, while local notifications work in Expo Go.

for a middle

Explain why: push is tied to the app's identity and credentials, and expo-notifications throws on Android and warns on iOS inside Expo Go.

for a senior

Show the migration and the guardrails: a development build with push configured, an isRunningInExpoGo guard, and an end-to-end test from server to tap.

for a principal

Set a team rule for which features may be developed in Expo Go at all, so parity gaps like push are caught before feature work starts.

## The scenario A team building a reminder app adds push notifications: the server sends "time for your evening check-in" to each device. Everything works in the team's heads, but in Expo Go on an Android phone the app crashes when it asks for a push token, and on iOS the console fills with warnings. The team's first instinct is to debug the notification code. The real cause is the client they are testing in. ## Why push belongs to your own binary A remote push notification travels from a server through the platform's push service to **one specific app** on a device. The push service identifies that app by its **identity**: - the iOS bundle identifier and its push entitlement and credentials; - the Android package name and the Firebase project behind it. **Expo Go** is one app with Expo's identity, shared by every project that runs inside it. It cannot hold your push credentials or your identity, so a token obtained inside it would not represent your app in production. Expo therefore documents remote push as a development-build feature. ## What the library actually does in Expo Go | Platform | Asking for a push token in Expo Go | Local notifications | |---|---|---| | Android | **throws**: remote push was removed from Expo Go in SDK 53 | work | | iOS | logs a warning in development | work | In the `expo-notifications` source, the token paths (`getDevicePushTokenAsync`, which `getExpoPushTokenAsync` uses, plus topic subscription and the token listener) call a guard that throws on Android when running in Expo Go and warns on iOS in `__DEV__`. Importing the library in Expo Go also prints a general warning that its functionality is not fully supported there. ## Moving the feature to a development build 1. **Install the dev client**: `npx expo install expo-dev-client`. 2. **Configure notifications** in the app config as the library documents, including the Android Firebase configuration and iOS push capability. 3. **Build**: `npx expo run:android` and `npx expo run:ios` locally, or a development build on EAS. 4. **Install it** on the devices you test with. 5. **Start Metro** with `npx expo start`; with `expo-dev-client` installed it prints *Using development build* and its QR code opens your build instead of Expo Go. 6. **Test the full path**: permission prompt, token registration with your server, a push sent from the server, and tapping the notification. ## Keeping Expo Go usable for the rest of the team Designers or reviewers may still open the app in Expo Go. Guard the push registration so it does not crash there: - `isRunningInExpoGo()` from `expo` returns `true` only inside Expo Go. - `Constants.executionEnvironment` is not a reliable test here: its `storeClient` value covers Expo Go **or** a development build. ## Common mistakes - Treating the Android crash as a bug in the notification handler, and wrapping it in `try`/`catch` instead of switching clients. - Concluding from iOS's warning-only behaviour that push works in Expo Go on iOS; the token it returns is not your production app's identity. - Testing local notifications in Expo Go and assuming remote push behaves the same. - Forgetting that each change to push configuration is a native change, so the development build must be rebuilt. ## Why iOS and Android differ here The library's source shows the asymmetry directly: on Android the guard throws, on iOS it only warns during development. Expo removed Android remote push from Expo Go in SDK 53 and made the library fail loudly there, so nobody ships an Android push flow that was never tested in a real build. On iOS the library keeps a development-time warning. Neither platform gives you a token for **your** production app from inside Expo Go, so the practical rule is the same on both: remote push is developed and tested in a development build. ## Definition of done for the push feature - A development build with the push configuration is installed on both platforms. - The permission prompt appears at a sensible moment, and denial is handled. - The server stores the token and can send to it. - A notification arrives with the app in the foreground, in the background and killed, and tapping it opens the right screen.

  • Why is Constants.executionEnvironment not a good way to detect Expo Go before registering for push?
    Its `storeClient` value is documented as Expo Go or a development build with `expo-dev-client`, so it cannot tell the two apart. `isRunningInExpoGo()` from `expo` checks for Expo Go's own native module and returns `true` only there.
  • Can scheduled local notifications be tested in Expo Go?
    Yes. Local notifications are scheduled and shown on the device without a push service or your credentials, so `expo-notifications` supports them in Expo Go. Only remote push, delivered from a server, needs a development build.

saying these in an interview costs you the question

  • Wrap getExpoPushTokenAsync in try/catch and push works in Expo Go
  • iOS only warns, so remote push is fine to test in Expo Go on iOS
  • Expo Go cannot schedule local notifications either
  • Constants.executionEnvironment reliably detects Expo Go
  • Push configuration changes only need a Metro reload