Is a Day Always 86,400 Seconds?
What looked like a Hermes engine crash turned out to be a one-line bug in addDays.
Is a Day Always 86,400 Seconds?
Crash Reports Poured In
The day after integrating Sentry, multiple crash reports came in. All the same pattern.
EXC_BAD_ACCESS: KERN_INVALID_ADDRESS
hermes::vm::DictPropertyMap::findOrAdd
hermes::vm::JSObject::getComputedPrimitiveDescriptor
Memory access violation deep inside Hermes VM. Native crash, no JS stack trace. It would have been easy to dismiss as a “Hermes GC bug.”
Days of Chasing Ghosts
At first, we genuinely believed it was a Hermes engine bug. Every stack trace pointed to hermes::vm:: internals. Was the GC mismanaging memory? Was the data too large?
We stabilized array references in weekTasksMap. Reduced FlatList windowSize. Changed extraData from a Map to a version counter. Deferred holiday data fetching with InteractionManager. We tried everything we could think of to reduce memory pressure on the calendar screen.
The crashes kept coming. And we couldn’t reproduce them. Because this bug never happens in the Korean timezone.
It wasn’t until we integrated Sentry that we got the real clue.
The Clue Was in the Console Error
Looking closely at Sentry breadcrumbs, a JS error was logged right before each native crash.
TypeError: Cannot read property 'startDate' of undefined
at WeekCalendarDepiction
at toWeekDepictions
at CalendarScreen (useState)
The calendar screen was crashing during initialization of weekly calendar data. This JS error triggered React Native’s native error handling, which then caused the Hermes crash.
Null Where Null Should Be Impossible
The problematic code:
this.weekIdInMonth = weekInterval
.getIntersection(monthInterval)!
.startDate.toISOString();
This computes the intersection between a week and a month interval, with ! asserting “this is never null.” Logically, since we derive the month interval from the week’s start date, they must always overlap.
But null it was.
The Trap in addDays
Tracing the root cause led to the addDays function.
// Before
export const addDays = (d, days) =>
new Date(d.getTime() + days * 86400000);
One day equals 86,400,000 milliseconds. Usually true. But on DST transition days, a day is 23 hours or 25 hours.
In European timezones where clocks spring forward on the last Sunday of March:
- Adding one day to midnight (00:00) gives you 01:00 the next day (23-hour day)
- Subtracting one day from midnight gives you 23:00 the previous day
23:00 the previous day is technically a different date. Derive a month interval from this shifted date, and you get the wrong month. Week and month no longer overlap. getIntersection returns null. Crash.
The Fix Was One Line
// After
export const addDays = (d, days) => {
const result = new Date(d);
result.setDate(result.getDate() + days);
return result;
};
setDate operates on calendar dates, not milliseconds. It handles DST transitions correctly, always.
Looking Back
- What looks like a native engine crash can be a JS-level bug
- The
new Date(d.getTime() + ms)pattern is DST-unsafe - “Logically, this can never be null” is a dangerous assumption — the environment can break your logic
- Without Sentry’s breadcrumbs, we would have written this off as a Hermes bug and moved on
- We couldn’t reproduce the bug because we develop in a timezone without DST
A day is not always 86,400 seconds.