skip to content

With expo-notifications on Android 13 or later, why must a notification channel exist before you request notification permission?

level: middleimportance: should knowfreq 30%

answer

  1. Android 13 prompt has a precondition
  2. at least one channel
  3. setNotificationChannelAsync first
  4. fallback channel is created lazily
  5. no-op returning null on iOS

basics

~10 s

Expo'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 s

On 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 lines
typescript
import * 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

for a junior

Remember the order in an Expo app on Android: create a channel with setNotificationChannelAsync, then request permission, then fetch a push token.

for a middle

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.

for a senior

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.

for a principal

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