Expo hangs waiting for a promise on Android and iPhone, but not in remote debug mode

When my app fires up it runs AsyncStorage.getItem(‘authToken’).then(…) to try to revive the users’ session rather than re-authenticating the user. console logging shows things running right up to that point, but never returns to execute the .then(…) code. The app never loads up and eventually the app closes and I am returned to the Expo app launcher screen. My app works fine when I am debugging remotely in Chrome, but when I disable remote debugging, this behavior happens.

Any ideas what might be different in non-remote-debug vs. debug mode?

Here is my code:

class AppContainer extends Component {
    constructor(props) {
        super(props);
        this.state = {
            isLoading: true,
        }
    }
    
    componentWillMount() {
        console.log("Restoring Auth Token")
        this.restoreAuthToken()
    }

    restoreAuthToken = () => {
        console.log('check for authToken');
        try {
            AsyncStorage.getItem('authToken').then((authToken) => {
            console.log('validating authToken');
            if (authToken != null) {
                console.log('authToken found');
                console.log(this.props)
                this.props.dispatch(this.props.setAuthToken(authToken));
                this.props.dispatch(this.props.setAuthStatus(true));
            }
            else {
                console.log('authToken not found');
            }

            this.setState({isLoading: false});

            });
        }
        catch(error){
            console.log(error);
        }
    }

    render() {
        // console.log(this)

        // Wait for the restoreAuthToken to finish.
        if (this.state.isLoading) {
            console.log('Waiting...')
            return <Expo.AppLoading />
        }

        const { navigationState, dispatch } = this.props
        BackHandler.addEventListener('hardwareBackPress', this.backAction)

        if (this.props.isAuthenticated) {
            console.log('Calling AppNavigator');
            return <AppNavigator
                ref={(ref) => this.navigator = ref}
                navigation={
                    addNavigationHelpers({
                        dispatch: dispatch,
                        state: navigationState
                    })
                }
            />
        }
        else {
            console.log('Calling AuthNavigator')
            return <AuthNavigator
                ref={(ref) => this.navigator = ref}
                navigation={
                    addNavigationHelpers({
                        dispatch: dispatch,
                        state: navigationState
                    })
                }
            />
        }
    }

Running this code I see the following logging:

4:27:48 PM: Finished building JavaScript bundle in 409ms
4:27:54 PM: Running app on SM-G935V in development mode

4:27:55 PM: Restoring Auth Token

4:27:55 PM: check for authToken

4:27:55 PM: Waiting…

It never gets to the point where it logs: “validating authToken” after the promise is returned.

Also, to see if I could get my app running I disabled the code for caching the session token in AsyncStorage and it hung in the same way, but in a different spot waiting for a promise. Here it hangs in the same way at the line where it is trying to convert the response into json with resp.json(). It works fine when I am in remote JS debugging mode with Chrome.

export function authenticate(email, password) {
    console.log("authenticating...")
    
    return (dispatch, getState) => {
        return Api.post(`/api/v1/user/authenticate/`, '', getPostData(email, password))
            .then(resp => {
                switch (resp.status) {
                    case 200: {
                        console.log('authenticate returned 200')
                        console.log(resp)
                        resp.json().then(token => {
                            console.log('setAuthToken')
                            dispatch(setAuthToken(token.token));
                            console.log('setAuthStatus')
                            dispatch(setAuthStatus(true));
                            //storeAuthToken(token.token);
                        }).catch((error) => {
                            console.log("error processing response token");
                            console.log(error);
                        })
                        break;
                    }
                    default: {
                        console.log('authenticate returned ' + resp.status)
                        dispatch(setLoginFailed(true));
                        removeAuthToken()
                        break;
                    }
                }
            }).catch((ex) => {
                console.log("Error Authenticating");
                console.log(ex);
                throw (ex);
            })
    }
}

Here the last thing logged is “authenticate returned 200”.
It doesn’t log anything after that including the console.log(resp).

HOWEVER,

If I comment-out the entire resp.json(… section up through break; it will log the resp to the console and the application continues normally.

Ok, I need to refactor this question a bit. My question is the same but I figured out that it is not the promise that is causing the issue, it is the call to redux dispatch() that is causing this issue. I was thrown because the console.log() right before dispatch never fired and thought it hadn’t made it to the dispatch().

I’ll research it from that angle, but it still is baffling why it only works when I am in remote JS debugging mode on the device.


On another post I found a way to get at the log files in android and found where it called “time of death” on Expo:

9-26 17:33:06.871 1595 5410 I ActivityManager: Process host.exp.exponent (pid 11384) has died(1179,395)
09-26 17:33:06.871 1595 5410 D ActivityManager: cleanUpApplicationRecord – 11384
09-26 17:33:06.880 1595 5410 W ActivityManager: Force removing ActivityRecord{639bbaed0 u0 host.exp.exponent/.experience.ExperienceActivity t16648}: app died, no saved state
09-26 17:33:06.883 1595 2435 W InputDispatcher: channel ~ Consumer closed input channel or an error occurred. events=0x9
09-26 17:33:06.896 1595 2435 E InputDispatcher: channel ~ Channel is unrecoverably broken and will be disposed!
09-26 17:33:06.917 1595 5417 I WindowManager: WIN DEATH: Window{86e5b49d0 u0 host.exp.exponent/host.exp.exponent.experience.HomeActivity}
09-26 17:33:06.917 1595 5410 W MultiScreenManagerService: moveTaskBackToDisplayIfNeeded(): root is not base activity
09-26 17:33:06.922 1595 4595 D InputTransport: Input channel destroyed: fd=483
09-26 17:33:06.922 1595 5417 W InputDispatcher: Attempted to unregister already unregistered input channel
09-26 17:33:06.926 4491 4491 D InputTransport: Input channel destroyed: fd=81
09-26 17:33:06.934 7337 7363 I TrayUsageStatesWatcher: notePauseComponent : ComponentInfo{host.exp.exponent/host.exp.exponent.experience.ExperienceActivity}
09-26 17:33:06.936 1595 5410 D GameManagerService: sem_perfomance_mode: 0
09-26 17:33:06.940 16035 16035 D SamsungTTS: onDestroy()
09-26 17:33:06.965 1595 1960 W art : Suspending all threads took: 8.423ms
09-26 17:33:06.972 1595 5417 I WindowManager: Destroying surface Surface(name=host.exp.exponent/host.exp.exponent.experience.HomeActivity) called by com.android.server.wm.WindowStateAnimator.destroySurface:2847 com.android.server.wm.WindowStateAnimator.destroySurfaceLocked:1078 com.android.server.wm.WindowState.removeLocked:1776 com.android.server.wm.WindowManagerService.removeWindowInnerLocked:2876 com.android.server.wm.WindowManagerService.removeWindowLocked:2816 com.android.server.wm.WindowState$DeathRecipient.binderDied:2196 android.os.BinderProxy.sendDeathNotice:701
09-26 17:33:06.977 721 751 I SurfaceFlinger: id=6463 Removed IomeActivit (2/9)
09-26 17:33:06.978 721 1153 I SurfaceFlinger: id=6463 Removed IomeActivit (-2/9)
09-26 17:33:07.024 1595 5417 D InputTransport: Input channel destroyed: fd=418
09-26 17:33:07.026 1595 5416 I WindowManager: WIN DEATH: Window{aaf21fcd0 u0 host.exp.exponent/host.exp.exponent.experience.ExperienceActivity}
09-26 17:33:07.030 1595 1960 I art : Explicit concurrent mark sweep GC freed 113668(6MB) AllocSpace objects, 8(160KB) LOS objects, 15% free, 86MB/102MB, paused 14.059ms total 425.447ms
09-26 17:33:07.042 1595 5416 D InputDispatcher: Focus left window: 11384

Ok, I figured it out and it wasn’t anything close to what I thought.

I believe that the app worked find with Debug JS Remotely on Chrome because all processing / output to console happened within my PC. When I ran on the device, it choked on the console output with the WiFi round-trip through the Android/iPhone/iPad. I am guessing that some kind of watchdog killed the Expo process on the Android/Apple devices as the buffer overran.

I am using Redux to manage state. Redux renders/processes the components with each state change, so logging things like console.log(this) fired many many many times as the application state kept changing, thereby exacerbating the problem.

The fix is that I removed all console.log() statements. It immediately started to work on the mobile device without “Debug JS Remotely” turned on, and the performance improved dramatically. This page on performance tuning React-Native apps is what clued me in to the problem.

If you’re here because you have an app that works fine while running in Debug JS Remotely mode but seizes up when running w/o debug mode, comment out all of your console.log() statements and see if that helps.