Skip to content

[notifications] fix background tasks not executing#43245

Merged
vonovak merged 1 commit intomainfrom
vonovak__notifications_fix_background_task_not_executing
Feb 18, 2026
Merged

[notifications] fix background tasks not executing#43245
vonovak merged 1 commit intomainfrom
vonovak__notifications_fix_background_task_not_executing

Conversation

@vonovak
Copy link
Copy Markdown
Contributor

@vonovak vonovak commented Feb 18, 2026

Why

closes #38223

The above issue didn't include full repro steps so here they are:

Prerequisites

  • An Expo app with expo-notifications and expo-task-manager configured
  • A registered BACKGROUND_NOTIFICATION_TASK via TaskManager.defineTask and Notifications.registerTaskAsync

Sending push notifications

Install expo-server-sdk:

mkdir /tmp/expo-push-test && cd /tmp/expo-push-test
npm init -y && npm install expo-server-sdk

Create send.mjs:

import { Expo } from "expo-server-sdk";

const expo = new Expo();
const token = "ExponentPushToken[YOUR_TOKEN_HERE]";
const mode = process.argv[2] // data (headless) or regular notification

const message = mode === "data"
  ? { to: token, data: { type: "background_task", timestamp: String(Date.now()) }, _contentAvailable: true }
  : { to: token, title: "Test", body: `Notification at ${new Date().toLocaleTimeString()}` };

const [chunk] = expo.chunkPushNotifications([message]);
console.log(`Sending ${mode} notification...`);
const result = await expo.sendPushNotificationsAsync(chunk);
console.log("Result:", JSON.stringify(result, null, 2));

Reproduction steps

  1. Launch the app and confirm it's in the foreground
  2. Send a display notification:
  3. Kill the app (swipe away from recents)
  4. Tap the notification from the system tray — the app opens
  5. Wait for the app to fully load
  6. Send a data-only notification:

Expected behavior

The BACKGROUND_NOTIFICATION_TASK executes and JS receives the notification.

Actual behavior (before fix)

The background task silently fails. All subsequent data-only notifications also fail — the task never runs again until the app is force-stopped and restarted.

Root cause

When tapping the notification in step 4, handleNotificationResponse called runTaskManagerTasks while the app was still launching. This started a headless React instance that raced with the foreground app. The foreground TaskManager got misclassified as headless (via isStartedByHeadlessLoader), then wiped by invalidateAppRecord — permanently breaking background task execution for the session.

How

Only call runTaskManagerTasks for non-default notification actions (custom action buttons like "Mark as read").

The default tap (DEFAULT_ACTION_IDENTIFIER) always opens the app, so there's no need for headless task execution — the foreground app handles the response via listeners.

The fix is in if (!isAppInForeground() && notificationResponse.actionIdentifier != NotificationResponse.DEFAULT_ACTION_IDENTIFIER) but I also resolved one older TODO around an awkward use of WeakHashMap.

Test Plan

  • tested on device using the repro steps

Checklist

Copy link
Copy Markdown
Contributor Author

vonovak commented Feb 18, 2026

This stack of pull requests is managed by Graphite. Learn more about stacking.

@expo-bot expo-bot added the bot: suggestions ExpoBot has some suggestions label Feb 18, 2026
@expo-bot
Copy link
Copy Markdown
Collaborator

expo-bot commented Feb 18, 2026

The Pull Request introduced fingerprint changes against the base commit: fe7ef63

Fingerprint diff
[
  {
    "op": "changed",
    "beforeSource": {
      "type": "dir",
      "filePath": "../../packages/expo-notifications/android",
      "reasons": [
        "expoAutolinkingAndroid"
      ],
      "hash": "329be3225a338cf67cdd5a9d92af0c4b6f7991ed"
    },
    "afterSource": {
      "type": "dir",
      "filePath": "../../packages/expo-notifications/android",
      "reasons": [
        "expoAutolinkingAndroid"
      ],
      "hash": "81000096661a6ce74df807d2a21d4677c5f64996"
    }
  },
  {
    "op": "changed",
    "beforeSource": {
      "type": "dir",
      "filePath": "../../packages/expo-task-manager/android",
      "reasons": [
        "expoAutolinkingAndroid"
      ],
      "hash": "53386b5446326692d70aa9ff11e321f2529a129f"
    },
    "afterSource": {
      "type": "dir",
      "filePath": "../../packages/expo-task-manager/android",
      "reasons": [
        "expoAutolinkingAndroid"
      ],
      "hash": "7ade474389f72c686df3908b8689efe52a007e9c"
    }
  }
]

Generated by PR labeler 🤖

@vonovak vonovak force-pushed the vonovak__notifications_fix_background_task_not_executing branch from 15453e8 to ebe48df Compare February 18, 2026 19:35
@expo-bot
Copy link
Copy Markdown
Collaborator

Hi there! 👋 I'm a bot whose goal is to ensure your contributions meet our guidelines.

I've found some issues in your pull request that should be addressed (click on them for more details) 👇

⚠️ Suggestion: Missing changelog entries


Your changes should be noted in the changelog, e.g.:
- fix background task not executing ([#43245](https://github.com/expo/expo/pull/43245) by [@vonovak](https://github.com/vonovak))
Read Updating Changelogs guide and consider adding an appropriate entry to the following changelogs:


Generated by ExpoBot 🤖 against ebe48df

@vonovak vonovak requested a review from douglowder February 18, 2026 20:10
@vonovak vonovak marked this pull request as ready for review February 18, 2026 20:10
@github-actions
Copy link
Copy Markdown
Contributor

Subscribed to pull request

File Patterns Mentions
packages/expo-notifications/** @douglowder
packages/expo-task-manager/** @chrfalch, @byCedric

Generated by CodeMention

@vonovak vonovak changed the title [notifications] fix background task not executing [notifications] fix background tasks not executing Feb 18, 2026
@vonovak vonovak merged commit 0e16952 into main Feb 18, 2026
19 of 20 checks passed
@vonovak vonovak deleted the vonovak__notifications_fix_background_task_not_executing branch February 18, 2026 20:51
benjaminkomen pushed a commit to benjaminkomen/expo that referenced this pull request Feb 25, 2026
# Why

closes expo#38223

The above issue didn't include full repro steps so here they are:

### Prerequisites

- An Expo app with `expo-notifications` and `expo-task-manager`
configured
- A registered `BACKGROUND_NOTIFICATION_TASK` via
`TaskManager.defineTask` and `Notifications.registerTaskAsync`

### Sending push notifications

Install `expo-server-sdk`:

```bash
mkdir /tmp/expo-push-test && cd /tmp/expo-push-test
npm init -y && npm install expo-server-sdk
```

Create `send.mjs`:

```js
import { Expo } from "expo-server-sdk";

const expo = new Expo();
const token = "ExponentPushToken[YOUR_TOKEN_HERE]";
const mode = process.argv[2] // data (headless) or regular notification

const message = mode === "data"
  ? { to: token, data: { type: "background_task", timestamp: String(Date.now()) }, _contentAvailable: true }
  : { to: token, title: "Test", body: `Notification at ${new Date().toLocaleTimeString()}` };

const [chunk] = expo.chunkPushNotifications([message]);
console.log(`Sending ${mode} notification...`);
const result = await expo.sendPushNotificationsAsync(chunk);
console.log("Result:", JSON.stringify(result, null, 2));
```

### Reproduction steps

1. **Launch the app** and confirm it's in the foreground
2. **Send a display notification:**
3. **Kill the app** (swipe away from recents)
4. **Tap the notification** from the system tray — the app opens
5. **Wait for the app to fully load**
6. **Send a data-only notification:**

### Expected behavior

The `BACKGROUND_NOTIFICATION_TASK` executes and JS receives the
notification.

### Actual behavior (before fix)

The background task silently fails. All subsequent data-only
notifications also fail — the task never runs again until the app is
force-stopped and restarted.

### Root cause

When tapping the notification in step 4, `handleNotificationResponse`
called `runTaskManagerTasks` while the app was still launching. This
started a headless React instance that raced with the foreground app.
The foreground `TaskManager` got misclassified as headless (via
`isStartedByHeadlessLoader`), then wiped by `invalidateAppRecord` —
permanently breaking background task execution for the session.

# How

Only call `runTaskManagerTasks` for non-default notification actions
(custom action buttons like "Mark as read").

The default tap (`DEFAULT_ACTION_IDENTIFIER`) always opens the app, so
there's no need for headless task execution — the foreground app handles
the response via listeners.



The fix is in `if (!isAppInForeground() &&
notificationResponse.actionIdentifier !=
NotificationResponse.DEFAULT_ACTION_IDENTIFIER)` but I also resolved one
older TODO around an awkward use of `WeakHashMap`.

# Test Plan

- tested on device using the repro steps

# Checklist

<!--
Please check the appropriate items below if they apply to your diff.
-->

- [x] I added a `changelog.md` entry and rebuilt the package sources
according to [this short
guide](https://github.com/expo/expo/blob/main/CONTRIBUTING.md#-before-submitting)
- [ ] This diff will work correctly for `npx expo prebuild` & EAS Build
(eg: updated a module plugin).
- [ ] Conforms with the [Documentation Writing Style
Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

expo-notifications, expo-task-manager: Headless Notifications Fail to Trigger Tasks Registered via Notifications.registerTaskAsync

3 participants