With expo-notifications on Android 13 or later, why must a notification channel exist before you request notification permission?
answer
- Android 13 prompt has a precondition
- at least one channel
- setNotificationChannelAsync first
- fallback channel is created lazily
- no-op returning null on iOS
basics
~10 sExpo's documentation states that on Android 13 the notification permission prompt will not appear until at least one channel exists. So call setNotificationChannelAsync before requestPermissionsAsync and before fetching a push token.
solid answer
~40 sOn Android 13+ the notification permission is the `POST_NOTIFICATIONS` runtime permission, and expo-notifications' documentation says its prompt **will not appear until at least one notification channel has been created**; it also says `setNotificationChannelAsync` must be called before `getDevicePushTokenAsync` or `getExpoPushTokenAsync`. expo-notifications does create a fallback channel named Miscellaneous, but only lazily, when it builds a notification that has no valid channel, which is far too late for the prompt. So the registration order is: create the app's real channel (for a habit tracker, a Reminders channel), then check and request permission, then get the token. The channel call is Android-only: on iOS it logs a debug message and resolves `null`, so it is harmless there, though a `Platform.OS` guard makes intent clear.
code
typescript · 18 linesimport * as Notifications from 'expo-notifications';
import { Platform } from 'react-native';
export async function prepareReminders(): Promise<boolean> {
if (Platform.OS === 'android') {
await Notifications.setNotificationChannelAsync('reminders', {
name: 'Habit reminders',
importance: Notifications.AndroidImportance.HIGH,
});
}
const current = await Notifications.getPermissionsAsync();
if (current.granted) return true;
if (!current.canAskAgain) return false;
const answer = await Notifications.requestPermissionsAsync();
return answer.granted;
}go deeper
Remember the order in an Expo app on Android: create a channel with setNotificationChannelAsync, then request permission, then fetch a push token.
Explain why the order matters: Expo documents that the Android 13 prompt waits for a channel, and its fallback channel is only created when a notification is built, too late for the prompt.
Recognise the symptom in production: an explainer that promises a dialog, no dialog, and reminders that silently never show. Fix it by moving channel creation to startup, not by retrying the request.
Treat notification bootstrap as one ordered, owned sequence (channel, permission, token) that every entry point reuses, instead of each feature assembling its own partial version.
## The rule, and where it comes from Android 8 made every notification belong to a **channel**, a named category whose sound, vibration and importance the user can control. Android 13 then made posting notifications a runtime permission, `POST_NOTIFICATIONS`. expo-notifications' Android documentation connects the two: > On Android 13, app users must opt-in to receive notifications via a permissions prompt ... This prompt will not appear until at least one notification channel is created. It adds that `setNotificationChannelAsync` must be called before `getDevicePushTokenAsync` or `getExpoPushTokenAsync` to obtain a push token. In practice that gives a fixed **registration order** for any Expo app that shows notifications on Android. ## The order that works 1. **Create the channel** with `Notifications.setNotificationChannelAsync(id, { name, importance })`. 2. **Read the state** with `Notifications.getPermissionsAsync()`. 3. **Request** with `Notifications.requestPermissionsAsync()` when the user is in context and `canAskAgain` is true. 4. **Fetch a token** only after that, if the app uses remote push at all. For a **habit tracker**, the channel is the natural "Reminders" channel the app needs anyway, so creating it early costs nothing and is not a placeholder. ## Why the fallback channel does not save you expo-notifications does have a safety net: if a notification is presented without a channel id, or with an id that does not exist, it falls back to a channel named **Miscellaneous**, creating it if needed. The Expo documentation recommends not relying on it and always creating channels with informative names. The important detail for this question is **when** it is created: | Moment | Channel exists? | |---|---| | App start, before any channel call | No | | `requestPermissionsAsync()` called first | No, so the prompt may not appear | | First notification built without a channel | Yes, the fallback is created now | The fallback appears only when a notification is built, and a notification cannot be shown before permission is granted. Relying on it leaves the prompt without the channel it needs. ## What going wrong looks like - The user taps "Remind me", the explainer says a dialog is coming, and **nothing appears**. - The permission state stays not granted, so reminders silently never show. - Push token code that runs before the channel exists breaks the order Expo documents too, so both call sites move together. The fix is ordering, not retrying: calling the request again without a channel changes nothing. ## Platform behaviour of the channel call - On **Android**, `setNotificationChannelAsync` creates or updates the channel and resolves the resulting channel object. - On **iOS** (and web), expo-notifications' non-Android implementation logs a debug message that channels are Android-only and resolves `null`. So the call can sit in shared code, but most codebases wrap it in `Platform.OS === 'android'` for readability. What the channel's importance, sound or vibration should be is a separate decision about the channel itself; the permission flow only needs the channel to exist. ## Where this sits in a bare app `POST_NOTIFICATIONS` is also requestable through core `PermissionsAndroid` in a bare app. The Expo documentation's channel rule is written for expo-notifications; a bare app that creates its channels through another library commonly follows the same order, creating channels at startup before any permission request. ## Where to put the call Calling `setNotificationChannelAsync` again with the same id on every start does not create a second channel, so app startup is a safe home for it: - run it once in the root layout or an app-level bootstrap function, before any screen can trigger the permission flow; - keep the channel ids in one module so the permission code, the scheduling code and any server payloads all agree; - do not tie it to the permission button, where a failure or an early return can skip it. Placing it at startup also means the first scheduled reminder already has a real channel, so the Miscellaneous fallback never appears in the user's system settings. ## Checklist - Create the app's real channel before `requestPermissionsAsync()`. - Create it before `getDevicePushTokenAsync()` or `getExpoPushTokenAsync()`. - Do not count on the Miscellaneous fallback for anything but a last resort. - Keep the call harmless on iOS: it resolves `null` there.
- Does the order still matter if the Expo app only schedules local reminders and never fetches a push token?Yes. The channel requirement in expo-notifications' documentation is about the Android 13 permission prompt itself, not about remote push. A local-only habit tracker still needs `POST_NOTIFICATIONS` to display its reminders, so it still creates its reminders channel before calling `requestPermissionsAsync()`.
saying these in an interview costs you the question
- The Miscellaneous fallback channel already covers the Android 13 prompt
- Channels only matter for remote push, not for the permission prompt
- Calling setNotificationChannelAsync on iOS throws, so it needs a try/catch
- If the Android prompt does not appear, just call the request again
- Fetch the push token first, then create channels once it arrives