NeuroAgent

Fix React Native Expo keyWindow Error After Upgrade

Learn how to fix the 'Cannot find the keyWindow' error in React Native/Expo after upgrading. Complete guide with code examples and troubleshooting steps.

Question

How to fix the ‘Fatal error: Cannot find the keyWindow. Make sure to call window.makeKeyAndVisible()’ error in React Native/Expo after upgrading?

I’m encountering this error after upgrading my Expo and React Native versions:

EXDevLauncher/ExpoDevLauncherAppDelegateSubscriber.swift:8: Fatal error: Cannot find the keyWindow. Make sure to call window.makeKeyAndVisible().

I’ve tried modifying the AppDelegate.m file without success. Below are my AppDelegate and SceneDelegate files:

AppDelegate.m:

objc
#import <RCTAppDelegate.h>
#import <UIKit/UIKit.h>
#import <Expo/Expo.h>

@interface AppDelegate : EXAppDelegateWrapper

@end

AppDelegate.m (implementation):

objc
#import "AppDelegate.h"
// @generated begin react-native-maps-import - expo prebuild (DO NOT MODIFY) sync-f2f83125c99c0d74b42a2612947510c4e08c423a
#if __has_include(<GoogleMaps/GoogleMaps.h>)
#import <GoogleMaps/GoogleMaps.h>
#endif
// @generated end react-native-maps-import

#import <React/RCTBundleURLProvider.h>
#import <React/RCTLinkingManager.h>

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
#if __has_include(<GoogleMaps/GoogleMaps.h>)
  [GMSServices provideAPIKey:@"AIzaSyCDnK85Y_BEl8g-tdrdSl8eC2VGotnEB5k"];
#endif
  
  self.moduleName = @"main";
  self.initialProps = @{};

  NSLog(@"[AppDelegate] didFinishLaunching begin");
  
  // Call super but WITHOUT creating a window (SceneDelegate will do that)
  BOOL result = [super application:application didFinishLaunchingWithOptions:launchOptions];
  NSLog(@"[AppDelegate] didFinishLaunching end");
  return result;
}

- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
#if DEBUG
  return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
#else
  return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
}

// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
  return [super application:application didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
}

// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
{
  return [super application:application didFailToRegisterForRemoteNotificationsWithError:error];
}

// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
  return [super application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler];
}

@end

main.m:

objc
#import <UIKit/UIKit.h>

#import "AppDelegate.h"

int main(int argc, char * argv[]) {
  @autoreleasepool {
    return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
  }
}

SceneDelegate.m:

objc
#import "SceneDelegate.h"
#import <EXDevLauncher/EXDevLauncherController.h>
#import <React/RCTBridge.h>
#import <React/RCTRootView.h>
#import "AppDelegate.h"

@implementation SceneDelegate

- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions {
  if (![scene isKindOfClass:[UIWindowScene class]]) { return; }
  UIWindowScene *windowScene = (UIWindowScene *)scene;

  NSLog(@"[SceneDelegate] willConnectToSession");

  // Create window for this scene
  self.window = [[UIWindow alloc] initWithWindowScene:windowScene];
  
  NSLog(@"[SceneDelegate] window created");
  
  // Get AppDelegate for dev launcher delegate
  AppDelegate *appDelegate = (AppDelegate *)UIApplication.sharedApplication.delegate;
  
  // Make window visible AFTER dev launcher is initialized
  [self.window makeKeyAndVisible];
  NSLog(@"[SceneDelegate] window makeKeyAndVisible called");
  
  // NOW start dev launcher with the window
  NSLog(@"[SceneDelegate] EXDevLauncherController startWithWindow called");
  EXDevLauncherController *controller = [EXDevLauncherController sharedInstance];
  [controller startWithWindow:self.window delegate:appDelegate launchOptions:nil];
  
  NSLog(@"[SceneDelegate] setup complete, window is key and visible");
}

- (void)sceneDidDisconnect:(UIScene *)scene {
  NSLog(@"[SceneDelegate] sceneDidDisconnect");
}

- (void)sceneDidBecomeActive:(UIScene *)scene {
  NSLog(@"[SceneDelegate] sceneDidBecomeActive");
}

- (void)sceneWillResignActive:(UIScene *)scene {
  NSLog(@"[SceneDelegate] sceneWillResignActive");
}

- (void)sceneWillEnterForeground:(UIScene *)scene {
  NSLog(@"[SceneDelegate] sceneWillEnterForeground");
}

- (void)sceneDidEnterBackground:(UIScene *)scene {
  NSLog(@"[SceneDelegate] sceneDidEnterBackground");
}

@end

AppDelegate+SceneConfiguration.m:

objc
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#import "SceneDelegate.h"

// This category supplies a UIScene configuration at runtime so the app
// uses SceneDelegate even if Info.plist doesn't declare UIApplicationSceneManifest.
@implementation AppDelegate (SceneConfiguration)

- (UISceneConfiguration *)application:(UIApplication *)application
 configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession
                              options:(UISceneConnectionOptions *)options
{
  UISceneConfiguration *config = [UISceneConfiguration configurationWithName:@"Default Configuration"
                                                                 sessionRole:connectingSceneSession.role];
  config.delegateClass = [SceneDelegate class];
  return config;
}

- (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet<UISceneSession *> *)sceneSessions
{
  // No-op. Implemented to fully adopt UIScene lifecycle.
}

@end

What is causing this error and how can I fix it in my React Native/Expo application after upgrading?

NeuroAgent

After upgrading your Expo and React Native versions, the “Fatal error: Cannot find the keyWindow” typically occurs due to timing issues between SceneDelegate initialization and EXDevLauncher’s attempt to access the window. This is a common problem when migrating to iOS 13+ Scene-based architecture, where the window lifecycle has changed significantly.


Contents


Understanding the Root Cause

The error occurs because EXDevLauncher expects to find an active keyWindow during initialization, but in modern iOS Scene-based architecture, the window isn’t immediately available. According to the Expo GitHub issue #23536, this happens specifically when UIApplicationSceneManifest is defined in your Info.plist for multiscreen support or CarPlay compatibility.

The core problem is a race condition between:

  1. SceneDelegate creating the window
  2. EXDevLauncher trying to access that window
  3. The makeKeyAndVisible() call timing

Key insight: The error message specifically points to EXDevLauncher/ExpoDevLauncherAppDelegateSubscriber.swift:8, indicating this is happening in Expo’s dev launcher code, not your custom code.


Immediate Solution: Fixing SceneDelegate Timing

The most reliable fix is to ensure the window is properly initialized and made key-and-visible before EXDevLauncher tries to use it. Here’s the corrected SceneDelegate.m:

objc
#import "SceneDelegate.h"
#import <EXDevLauncher/EXDevLauncherController.h>
#import <React/RCTBridge.h>
#import <React/RCTRootView.h>
#import "AppDelegate.h"

@implementation SceneDelegate

- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions {
  if (![scene isKindOfClass:[UIWindowScene class]]) { return; }
  UIWindowScene *windowScene = (UIWindowScene *)scene;

  NSLog(@"[SceneDelegate] willConnectToSession");

  // Create window for this scene
  self.window = [[UIWindow alloc] initWithWindowScene:windowScene];
  
  // CRITICAL: Make window visible BEFORE dev launcher initialization
  [self.window makeKeyAndVisible];
  NSLog(@"[SceneDelegate] window made key and visible");
  
  // Get AppDelegate for dev launcher delegate
  AppDelegate *appDelegate = (AppDelegate *)UIApplication.sharedApplication.delegate;
  
  // Now start dev launcher with the already-visible window
  NSLog(@"[SceneDelegate] EXDevLauncherController startWithWindow called");
  EXDevLauncherController *controller = [EXDevLauncherController sharedInstance];
  [controller startWithWindow:self.window delegate:appDelegate launchOptions:nil];
  
  NSLog(@"[SceneDelegate] setup complete");
}

Key changes made:

  1. Moved makeKeyAndVisible() call before EXDevLauncher initialization
  2. Removed potential race condition by ensuring the window is ready first
  3. Simplified the flow to avoid any intermediate states

Alternative Workarounds

Method 1: Delayed EXDevLauncher Initialization

If the above doesn’t work, add a small delay:

objc
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
  EXDevLauncherController *controller = [EXDevLauncherController sharedInstance];
  [controller startWithWindow:self.window delegate:appDelegate launchOptions:nil];
});

Method 2: Check for Existing Windows

Add a safety check in your AppDelegate:

objc
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  // Check if we have a valid key window
  UIWindow *keyWindow = UIApplication.sharedApplication.keyWindow;
  if (!keyWindow) {
    // Create a temporary window if none exists
    UIWindowScene *windowScene = [UIApplication.sharedApplication.connectedScenes anyObject];
    if ([windowScene isKindOfClass:[UIWindowScene class]]) {
      keyWindow = [[UIWindow alloc] initWithWindowScene:(UIWindowScene *)windowScene];
      [keyWindow makeKeyAndVisible];
    }
  }
  
  // Continue with normal initialization
  return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

Method 3: Update Expo Dependencies

According to the Expo upgrade saga, ensure you’re using compatible versions:

bash
npx expo install expo@latest expo-dev-client@latest

Prevention and Future-Proofing

1. Use Modern iOS Architecture

Ensure your app fully adopts SceneDelegate pattern:

Info.plist should include:

xml
<key>UIApplicationSceneManifest</key>
<dict>
  <key>UIApplicationSupportsMultipleScenes</key>
  <false/>
  <key>UISceneConfigurations</key>
  <dict>
    <key>UIWindowSceneSessionRoleApplication</key>
    <array>
      <dict>
        <key>UISceneConfigurationName</key>
        <string>Default Configuration</string>
        <key>UISceneDelegateClassName</key>
        <string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
      </dict>
    </array>
  </dict>
</dict>

2. Handle Window Lifecycle Properly

Add window lifecycle management:

objc
- (void)sceneDidBecomeActive:(UIScene *)scene {
  if ([scene isKindOfClass:[UIWindowScene class]]) {
    [self.window makeKeyAndVisible];
  }
}

3. Monitor Expo Updates

Subscribe to Expo GitHub issues for similar problems. The community often provides patches before official releases.


Advanced Troubleshooting

If you’re still experiencing issues:

Check for Conflicting Libraries

Some libraries might interfere with window initialization. Temporarily remove third-party libraries to isolate the issue.

Debug with Breakpoints

Add breakpoints in SceneDelegate.m to trace the exact sequence:

objc
// Add these to willConnectToSession
NSLog(@"[DEBUG] About to create window");
// Breakpoint here
NSLog(@"[DEBUG] Window created: %@", self.window);
// Breakpoint here
NSLog(@"[DEBUG] About to make key and visible");
// Breakpoint here

Verify Xcode Project Settings

Ensure your Xcode project has:

  • Deployment Target: iOS 13.0+
  • Application Scene Manifest: Enabled
  • Uses Scenes: Yes

Clean and Rebuild

bash
npx expo start --clear
rm -rf ios/build
cd ios && xcodebuild clean -scheme YourApp -configuration Debug

Conclusion

The “Cannot find the keyWindow” error after upgrading Expo is typically caused by timing issues in the Scene-based architecture. The most effective solution is to ensure your window is properly initialized and made visible before EXDevLauncher attempts to use it.

Key takeaways:

  1. Move makeKeyAndVisible() call before EXDevLauncher initialization in SceneDelegate
  2. Ensure proper Info.plist configuration with UIApplicationSceneManifest
  3. Keep Expo dependencies updated to the latest compatible versions
  4. Add proper error handling for window lifecycle events
  5. Monitor community discussions for similar issues and workarounds

If you continue to experience problems, consider checking the Expo GitHub issues or reaching out to the community on Reddit r/reactnative for specific version troubleshooting.


Sources

  1. Expo GitHub Issue #23536 - expo-dev-launcher breaks with UIApplicationSceneManifest
  2. Medium - Expo Modules Upgrade Saga (Expo 51 → 53)
  3. Stack Overflow - How to fix keywindow after upgrading expo and react
  4. Apple Developer Forums - iOS 15: UIApplication.shared.keyWindow is nil
  5. Stack Overflow - ‘keyWindow’ was deprecated in iOS 13.0