React Native

React Native App Stuck on Splash Screen

An app stuck permanently on its splash screen almost always means something during app initialization is failing silently, or the code responsible for explicitly hiding the splash screen is never actually being reached or called.

The Problem

The app launches, shows its splash screen as expected, but never transitions to the actual app content — it just stays there indefinitely, with no crash, no error screen, and no obvious indication of what's wrong.

Why It Happens

Unlike a crash, a stuck splash screen typically means execution reached a point where something is silently failing or blocking, without triggering an error boundary or crash report. Common causes:

  • An async initialization step (loading fonts, fetching remote config, checking authentication state) throws an error that's caught somewhere but not properly handled, leaving the app in limbo without ever calling the code that would hide the splash screen and render the main UI
  • With Expo's splash screen module specifically, the code responsible for calling SplashScreen.hideAsync() is never reached, often because it's placed after an await on something that never resolves or rejects
  • A native-level crash or hang occurs before JavaScript even starts running, which produces no JavaScript-visible error at all, since the problem is happening before your JS error handling would even have a chance to run
  • An infinite loop or a genuinely hanging network request in initialization code, with no timeout, so the app just waits forever for something that will never complete

The Fix

First, if you're using Expo's splash screen API, confirm you're actually preventing auto-hide and then explicitly hiding it once your app is ready — skipping either half of this pattern causes issues:

import * as SplashScreen from 'expo-splash-screen';

SplashScreen.preventAutoHideAsync();

function App() {
  const [isReady, setIsReady] = useState(false);

  useEffect(() => {
    async function prepare() {
      try {
        await loadFonts();
        await checkAuthState();
      } catch (e) {
        console.warn(e); // log, but don't let it block forever
      } finally {
        setIsReady(true);
        await SplashScreen.hideAsync();
      }
    }
    prepare();
  }, []);

  if (!isReady) return null;
  return <MainApp />;
}

The key detail here is the try/catch/finally structure — using finally ensures hideAsync() gets called whether initialization succeeded or failed, rather than only on the success path, which is a common gap that leaves the splash screen stuck specifically when something goes wrong during startup rather than when everything works.

Add explicit timeouts to any network requests made during initialization, so a genuinely hanging request doesn't block the app forever:

const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10000);

try {
  const response = await fetch(configUrl, { signal: controller.signal });
} catch (e) {
  console.warn('Config fetch failed or timed out:', e);
} finally {
  clearTimeout(timeout);
}

If the issue happens before JavaScript even loads (a native-level hang), check native logs directly rather than relying on JavaScript console output, since a native crash or hang at this stage produces no JS-visible error at all:

# Android
adb logcat *:E

# iOS - use Xcode's device console, or Console.app for a physical device

Temporarily add logging at the very start of your app's entry point, before any async work begins, to confirm JavaScript is actually starting execution at all — if this log never appears, the problem is at the native level, not in your JavaScript initialization logic, and debugging should shift toward native build/configuration issues rather than app-level code.

Still Not Working?

If logging confirms JavaScript starts but never reaches the point where the splash screen should hide, add logging immediately before and after each individual async step in your initialization sequence, rather than only at the very start and end. This isolates exactly which specific step is hanging or throwing silently, turning a vague "stuck somewhere during startup" into a precise, fixable location in the code.