RORK LABJP
BUILD — Rork Max runs real Macs in the cloud loaded with Xcode and the iOS SDK, writing SwiftUI, compiling, reading the errors and building again. That loop, not the code generation, is what lifts the outputNATIVE — What comes out is pure Swift and SwiftUI, not React Native. Reaching AR, Metal graphics and widgets that React Native cannot touch is the real gap between this and other buildersPLATFORMS — Coverage spans iPhone, iPad, Apple Watch, Apple TV and Vision Pro, plus iMessage. Worth a look if you want to start from a watch app or an extension rather than a phone screenCOMPANION — The Rork Companion app lets you check a generated build on a real iPhone without a paid Apple Developer account, lowering the bar for trying a first project end to endPRICING — Free to start, paid plans from $25 a month, and Rork Max on the $200 Max plan. Worth working out up front how many projects it takes to earn that backDEADLINE — From August 31, 2026, Google Play requires target API level 36 or higher for new apps and updates alike. Ten days out, and the targetSdkVersion of what you generate is yours to verifyBUILD — Rork Max runs real Macs in the cloud loaded with Xcode and the iOS SDK, writing SwiftUI, compiling, reading the errors and building again. That loop, not the code generation, is what lifts the outputNATIVE — What comes out is pure Swift and SwiftUI, not React Native. Reaching AR, Metal graphics and widgets that React Native cannot touch is the real gap between this and other buildersPLATFORMS — Coverage spans iPhone, iPad, Apple Watch, Apple TV and Vision Pro, plus iMessage. Worth a look if you want to start from a watch app or an extension rather than a phone screenCOMPANION — The Rork Companion app lets you check a generated build on a real iPhone without a paid Apple Developer account, lowering the bar for trying a first project end to endPRICING — Free to start, paid plans from $25 a month, and Rork Max on the $200 Max plan. Worth working out up front how many projects it takes to earn that backDEADLINE — From August 31, 2026, Google Play requires target API level 36 or higher for new apps and updates alike. Ten days out, and the targetSdkVersion of what you generate is yours to verify
Articles/Dev Tools
Dev Tools/2026-06-22Advanced

When a Poisoned Cache Crashes Your App on Every Launch — Designing a Safe-Mode Boot Your Users Can Escape On Their Own

When a persisted cache goes bad and the app crashes at the same spot on every launch, the only option left to the user is to reinstall. This article designs a safe-mode boot for Expo (React Native): the app counts its own early crashes, confirms a launch only once it becomes interactive, and resets just the dangerous state in graduated steps.

Rork539Expo175React Native227Crash ResilienceMMKV6Architecture22

Premium Article

Running several apps on my own as an indie developer, I occasionally get a report that reads like a dead end: on one particular device, the app crashes the instant it launches and never opens again. I have hit this myself. A piece of persisted settings had turned into a corrupt record, the restore step on startup threw an exception right there, and from then on the app died at the same spot no matter how many times it was reopened.

What makes it brutal is how few options the user has left. The app won't open, the settings screen is unreachable, so there is nothing to do but uninstall and reinstall. And a reinstall is exactly the kind of moment that produces the harshest line in a review.

This article is about not fixing that broken state by hand after the fact, but designing a safe-mode boot: the app itself recognizes that it keeps crashing early, throws away only the dangerous state in graduated steps, and stands back up.

Why ErrorBoundary and Crashlytics Can't Get You Out

Let me first place where the usual defenses fall short on this specific problem.

ErrorBoundary is powerful, but it only protects the inside of the React render tree. Most boot loops happen while a provider is rehydrating a persisted store, or during native module initialization. If the crash lands before the tree mounts, the boundary has nothing to catch. The fundamentals of catching exceptions in React — down to unhandled promises — are covered in designing an ErrorBoundary that doesn't drop unhandled promises, but a crash before mount is still out of its reach.

Crashlytics records the crash, but the record is after the fact. It does not stop the loop on the user's device. This is also different from an iOS 0x8badf00d watchdog termination, where the OS kills you for blocking the main thread. Here the code is running correctly — the persisted data it was handed is the thing that is broken. Fixing the code doesn't save a device that already holds the poisoned state.

So what you need is neither observation nor capture, but recovery logic that runs on its own on the device. It helps to think of it as taking the white-screen-versus-crash triage at launch one step further and letting the app perform that triage itself.

The Core Idea: Count a Launch as Unconfirmed Until It Becomes Interactive

The mechanism at the center is simple.

The moment a launch begins, increment an "unconfirmed launch" counter by one. When the app actually reaches an interactive state — the first screen has rendered and the user can touch it — reset that counter to zero. Call this "confirming the launch."

If the app crashes before it becomes interactive, the confirm never runs. The counter stays elevated. The next launch increments it again, and if it crashes again, the count keeps building. When that happens enough times in a row, you conclude that this device has entered a boot loop.

import { MMKV } from 'react-native-mmkv'
 
const store = new MMKV({ id: 'boot-guard' })
const KEY_PENDING = 'boot.pending'      // consecutive unconfirmed launches
const KEY_LAST = 'boot.lastStartAt'     // timestamp of the most recent launch
const FAILED_BOOT_THRESHOLD = 3         // this many consecutive failures -> safe mode
const RECENT_WINDOW_MS = 30_000         // launches farther apart aren't "consecutive"
 
export type BootDecision = { safeMode: boolean; failedBoots: number }
 
// Call at the very top of the entry, before mounting any provider
export function beginBoot(): BootDecision {
  const now = Date.now()
  const lastStart = store.getNumber(KEY_LAST) ?? 0
  let pending = store.getNumber(KEY_PENDING) ?? 0
 
  // Not a quick succession -> not a boot loop. Start counting over.
  if (now - lastStart > RECENT_WINDOW_MS) pending = 0
 
  store.set(KEY_PENDING, pending + 1)
  store.set(KEY_LAST, now)
 
  // Before this launch even starts, have we already failed the threshold?
  return { safeMode: pending >= FAILED_BOOT_THRESHOLD, failedBoots: pending }
}
 
// Call once the app is interactive. This clears the consecutive-failure count.
export function confirmBoot() {
  store.set(KEY_PENDING, 0)
}

The RECENT_WINDOW_MS gate exists to avoid false positives. A user who opens the app, closes it right away, and opens it again days later is not in a crash loop. Only launches that pile up in quick succession count as one.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
Escaping a boot loop that neither ErrorBoundary nor Crashlytics can break — by letting the app notice and recover on its own
Counting a launch as unconfirmed until the app becomes interactive, and why only synchronous storage (MMKV) can record that counter reliably
A graduated reset ladder that minimizes blast radius, with a hard rule never to touch user-created data
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

Related Articles

Dev Tools2026-08-04
Exactly 20 Characters, Still Rejected: Putting Character Counting Behind One Boundary
Emoji and combining marks make your client and your server disagree about length. I measured four counting methods on Node v22 and found 75.6% of naive truncations split a grapheme and 36.1% produced replacement characters over UTF-8. Here is the shared-module design that fixed it.
Dev Tools2026-08-01
Every Experiment Kept Landing on the Same Devices: Hash Choice and Salt Position, Measured Across a Million IDs
On-device experiment assignment looked fine in isolation, then collapsed the moment two experiments ran side by side. Here is what one million IDs revealed about four hash functions, why the culprit was the concatenation order rather than hash quality, and the implementation I settled on, with the measured numbers.
Dev Tools2026-07-30
What Renovate may bump in an Expo app, and what it must never touch
Turning on automated dependency updates in a Rork-generated app also hands Renovate the 123 packages Expo SDK 57 pins. Measured on 2026-07-30, six of them sit a full major version behind npm latest. Here is how to generate the ignore list from the SDK instead of maintaining it by hand.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →