Moment: React-Native Release Builds, Deep Crash when changing locales

Created on 10 Oct 2019  ·  4Comments  ·  Source: moment/moment

When using react-native 0.59.10, and setting the locale for momentJS, we caught a super brutal crash for our release-builds only. This crash wasn't reproducible for us with the debugger attached. This crash occurred for us both on iOS & Android. try-catch statements wrapping all moment usage did not catch the crash!

To Reproduce

  1. We collect the locales/languages we want to try via Application-specific logic. Eg ["fr-CA", "en-US", "fr", "en"]
  2. We loop through these, one at a time rather than using the Array setter, so that we can call some other instrumentation, and potentially catch any JS Exceptions that are thrown and try the next candidate.
  3. Despite calling moment.locale(localeCandidate) inside a try-catch block, the Application still crashes on this line⁇

This was a crash-on-launch but only for Release builds!! This made it super tricky to extract useful error messages / logging.

We saw the following error messages via our Bugsnag integration & System Console Logging

  • iOS: Exception in HostFunction: Error loading module0from RAM Bundle: unspecified iostream_category error
  • Android: Exception in HostFunction: Module not found: 0
  • A day later, on iOS & Android we also saw Requiring unknown module "./locale/en-us". -- but strangely, this error was not being processed in a timely manner. Possibly a react-native / bugsnag issue.
  1. Eventual tracing found a single point that caused the crash:
    https://github.com/moment/moment/blob/96d0d6791ab495859d09a868803d31a55c917de1/moment.js#L1852-L1853
    Which I believe comes from here: https://github.com/moment/moment/blob/6a06e7a0db2c83fb92aa72bbf6bde955d4c75a16/src/lib/locale/locales.js#L55-L56

Workaround: Commenting out those two lines halted the crash!

Expected behaviors

  • Moment shouldn't behave crashily, ever, but especially not in release-builds-only!
  • Moment's exceptions should be catchable (our catch statements would have prevented a crash). -- (could be a react-native issue when messing with require() in Release)

Smartphone (please complete the following information):

  • Device: iPhone X, iPhone 11 Pro, Samsung (? -- didn't capture exactly what), Google Pixel 3, (react-native 0.59.10)
  • OS: iOS and Android
  • iOS JavaScript Core (for the below mentioned iOS versions), and jsc-android(+intl) 245459.0.0
  • Versions: iOS 12.4 & iOS 13.1, iOS 13.1.2, and iOS 13.2 (beta?), Android 28, others.

Moment-specific environment

  • Pacific Time, and Possibly London time
  • Code bug exhibited consistently for the past 2 weeks (2019-10-09) at all times of day.
  • Other libraries in use: moment-timezone and moment-with-locales, TypeScript, react-native 0.59.10, Apollo-Client & others

Please run the following code in your environment and include the output:

        console.log([
            new Date().toString(),
            new Date().toLocaleString(),
            new Date().getTimezoneOffset(),
            navigator && navigator.userAgent, // react-native might not have a navigator
            moment.version,
        ]);

Output:

[
  "Wed Oct 09 2019 18:52:16 GMT-0700 (PDT)",
  "09/10/2019 à 18:52:16", // This particular device is configured as fr-FR
  420,
  null,
  "2.24.0"
]

Additional context

Related Tickets

  • #5214 - Different because there's less context here, and using a different API and getting a different Error Message. Similar because it only happens in release builds, which suggests react-native as well, but they only reproed it on ios-simulator/ios.
  • #3872
  • #2979

Most helpful comment

The referenced lines are "automatically" trying to require modules at runtime, but the loading locales docs indicate that if you're using a Package Manager like JSPM, you can load locales by import "moment/locale/fr. Since we need the react-native package manager to "know" that the file must be imported, we tried that style of import so that the Packager can "see" all files that have to be bundled in.

Ultimately, our import lines looked like this:

import moment from "moment";
import "moment/min/locales"; // Import all moment-locales -- it's just 400kb
import "moment-timezone";

The exact implementation of require() is injected by the runtime you're working with, and that's definitely something that behaves significantly differently between Debug & Release builds.

In react-native, there are also several different flavors of Release-mode JavaScript Bundling, including all-in-one-file, all-in-separate-files, and RAM Bundles. Each of these also change how require works. Debug require() connects to the Metro Bundler running on a local http server. This is probably is very similar to the webpack/jspm/other debug servers, which is probably why aliasing require doesn't cause problems in that environment.

All 4 comments

The referenced lines are "automatically" trying to require modules at runtime, but the loading locales docs indicate that if you're using a Package Manager like JSPM, you can load locales by import "moment/locale/fr. Since we need the react-native package manager to "know" that the file must be imported, we tried that style of import so that the Packager can "see" all files that have to be bundled in.

Ultimately, our import lines looked like this:

import moment from "moment";
import "moment/min/locales"; // Import all moment-locales -- it's just 400kb
import "moment-timezone";

The exact implementation of require() is injected by the runtime you're working with, and that's definitely something that behaves significantly differently between Debug & Release builds.

In react-native, there are also several different flavors of Release-mode JavaScript Bundling, including all-in-one-file, all-in-separate-files, and RAM Bundles. Each of these also change how require works. Debug require() connects to the Metro Bundler running on a local http server. This is probably is very similar to the webpack/jspm/other debug servers, which is probably why aliasing require doesn't cause problems in that environment.

My Fix Proposals:

A. Delete the aliasedRequire entirely if that's not how you're supposed to do things any more + tweak install instructions about it?
B. Detect react-native vs browser (navigator isn't available in react-native, but there are other techniques here), and behave differently depending on which situation we find ourselves in? eg. if react-native && DEV then print a console.error if the locale is theoretically supported, but hasn't been required yet (+ update docs).
C. Move the aliasedRequire from a local variable in that function to a "semi-global". moment.aliasedRequire, that way we could inject a no-op/do-nothing function so that aliasedRequire can't cause react-native to crash hard anymore.

I'd be happy to implement any of these options if a maintainer can point me at which one they would like me to implement, and for Proposals B/C help me refine which exact implementation they would be inclined to accept!

@marwahaha -- not sure what the process is for Moment. Would you have an opinion on my fix proposals? I'd be happy to implement a PR once I get some advice on which route might be acceptable to contributors/maintainers?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

ghost picture ghost  ·  3Comments

slavafomin picture slavafomin  ·  3Comments

chitgoks picture chitgoks  ·  3Comments

dogukankotan picture dogukankotan  ·  3Comments

alvarotrigo picture alvarotrigo  ·  3Comments