Skip to content

Instantly share code, notes, and snippets.

@airowe
Created September 20, 2026 18:30
Show Gist options
  • Select an option

  • Save airowe/6d47f56e6a9bf5bcd5e65a7e396a72cc to your computer and use it in GitHub Desktop.

Select an option

Save airowe/6d47f56e6a9bf5bcd5e65a7e396a72cc to your computer and use it in GitHub Desktop.
expo-calendar 15.0.8: recurrenceRule: null crashes the Android app process (minimal repro)
import { useState } from 'react';
import { Button, StyleSheet, Text, View } from 'react-native';
import * as Calendar from 'expo-calendar';
export default function App() {
const [log, setLog] = useState('Press a button.');
async function firstWritableCalendarId() {
const { status } = await Calendar.requestCalendarPermissionsAsync();
if (status !== 'granted') throw new Error('Calendar permission denied');
const calendars = await Calendar.getCalendarsAsync(
Calendar.EntityTypes.EVENT
);
const writable = calendars.find(c => c.allowsModifications);
if (!writable) throw new Error('No writable calendar on this device');
return writable.id;
}
const base = {
title: 'repro event',
startDate: new Date(Date.now() + 86400000),
endDate: new Date(Date.now() + 86400000 + 3600000),
};
// CRASHES the app process on Android.
async function withNull() {
const calendarId = await firstWritableCalendarId();
setLog('calling createEventAsync with recurrenceRule: null ...');
const id = await Calendar.createEventAsync(calendarId, {
...base,
recurrenceRule: null,
});
setLog(`created ${id} (did not crash)`);
}
// Identical, but the key is omitted. Works.
async function withoutKey() {
const calendarId = await firstWritableCalendarId();
const id = await Calendar.createEventAsync(calendarId, base);
setLog(`created ${id}`);
}
const run = (fn: () => Promise<void>) => () => {
fn().catch(e => setLog(`rejected: ${e}`));
};
return (
<View style={styles.container}>
<Text style={styles.log}>{log}</Text>
<Button title="1. recurrenceRule: null (crashes)" onPress={run(withNull)} />
<View style={styles.gap} />
<Button title="2. key omitted (works)" onPress={run(withoutKey)} />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
padding: 24,
backgroundColor: '#fff',
},
log: { marginBottom: 24, fontSize: 14 },
gap: { height: 16 },
});

expo-calendar 15.0.8 — recurrenceRule: null crashes the Android app process

Minimal reproducible example. Two buttons issue the same createEventAsync call; the only difference is whether recurrenceRule is present with a null value or absent entirely.

  • Button 1 — recurrenceRule: null → app process dies (uncaught NPE in the native module)
  • Button 2 — key omitted → event is created normally

Reproduce

npx create-expo-app@latest repro -t expo-template-blank-typescript@sdk-54
cd repro
npx expo install expo-calendar          # installs 15.0.8 on SDK 54
# add the expo-calendar plugin + an android package name to app.json (see below)
# replace App.tsx with the file in this gist
npx expo prebuild --platform android --clean
npx expo run:android

app.json additions:

{
  "expo": {
    "plugins": [["expo-calendar", { "calendarPermission": "Repro needs calendar access." }]],
    "android": { "package": "com.example.calendarnullrepro" }
  }
}

The device needs at least one writable calendar. On a bare emulator there is none; create one with:

adb shell content insert \
  --uri 'content://com.android.calendar/calendars?account_name=repro%40local&account_type=LOCAL&caller_is_syncadapter=true' \
  --bind account_name:s:repro@local --bind account_type:s:LOCAL \
  --bind name:s:ReproCal --bind calendar_displayName:s:ReproCal \
  --bind calendar_access_level:i:700 --bind visible:i:1 \
  --bind sync_events:i:1 --bind calendar_timezone:s:UTC \
  --bind ownerAccount:s:repro@local

Then grant permissions and tap button 1:

adb shell pm grant com.example.calendarnullrepro android.permission.READ_CALENDAR
adb shell pm grant com.example.calendarnullrepro android.permission.WRITE_CALENDAR

Result

FATAL EXCEPTION: DefaultDispatcher-worker-2
Process: com.example.calendarnullrepro, PID: 20145
java.lang.NullPointerException: Attempt to invoke interface method
  'boolean expo.modules.core.arguments.ReadableArguments.containsKey(java.lang.String)'
  on a null object reference
    at expo.modules.calendar.CalendarModule.saveEvent(CalendarModule.kt:483)
    at expo.modules.calendar.CalendarModule.access$saveEvent(CalendarModule.kt:36)
    at expo.modules.calendar.CalendarModule$definition$lambda$38$lambda$19$lambda$18$$inlined$launchAsyncWithModuleScope$1.invokeSuspend(CalendarModule.kt:252)
ActivityManager: Process com.example.calendarnullrepro (pid 20145) has died

Button 2 on the same build prints created 24 and the app stays alive.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment