You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
TestFlight build 1.0.6(15) — the first build with PR #1514's iOS 26 TurboModule fix merged — launches, then crashes ~0.8s later. Unlike the original crash (which was in ObjCTurboModule::performVoidMethodInvocation), this one fires inside expo-updates' deliberate recovery-pipeline abort:
Reading the source: expo-updates runs waitForRemoteUpdate → launchCached → crash when it catches an initial error. On a fresh install where the remote server has nothing matching and the cache is empty, the pipeline blows straight through all three steps. The crash is not the bug — it's the symptom. Something else throws the initial error into expo-updates, and then expo-updates abends the app deliberately because it can't recover.
Ruled out
Stale OTA bundle mismatch. The EAS Updates dashboard shows all recent published updates tagged runtime 1.0.0. Our binary is runtime 1.0.6. A runtime-version mismatch means the server returns "no update" to the device — it never attempts to load a bundle — so a bad bundle can't be the trigger.
The plan
Temporarily disable expo-updates. This removes the ErrorRecovery pipeline from the startup path entirely. Whatever throws the initial error is no longer caught and converted into a deliberate abort() — it either propagates into a more informative crash signature or the app launches successfully.
Three possible outcomes on the next TestFlight build:
App reaches the home screen → expo-updates + its recovery machinery was the proximate cause. Re-enable updates in a follow-up PR after publishing a fresh production bundle for the new runtime version.
App crashes with a completely different signature → the real bug is exposed in that new stack trace. Debug it directly.
Identical ErrorRecovery.crash() signature → would be very surprising since that code path requires enabled: true to activate. Would force a close reading of config.
Changes
Exactly two edits in app/app.json:
Field
Before
After
Why
version
1.0.6
1.0.7
TestFlight build distinctness + invalidates runtime namespace so a stray publish can't land a bundle on the diagnostic binary
Commit 9f33448c did exactly this ("fix: disable expo-updates to surface real launch crash + bump to 1.0.3") during an earlier crash investigation. Same diagnostic move; clean pattern in the repo.
Post-merge
eas build --platform ios --profile production — source compile of React-Core is still in play via the #1514 patch
Submit to TestFlight
Crash log or home screen — report back here
Rollback
git revert + rebuild. Harmless; only restores the old config.
Follow-up (out of scope for this PR)
Once the real bug is fixed and the app launches, a follow-up PR should:
Re-enable updates.enabled: true
Run eas update --branch production to publish a bundle against runtime 1.0.7
…ch error
Build 15 TestFlight crashed ~0.8s after launch inside
expo-updates' ErrorRecovery.crash() (ErrorRecovery.swift:277).
The iOS 26 TurboModule fix from #1514 worked — the old crash
signature is gone — but ErrorRecovery is now firing on a
completely different path.
The Updates dashboard shows zero published OTA bundles at
runtime 1.0.6 (all recent updates are runtime 1.0.0), so the
binary isn't being served a stale mismatched bundle. That
means the original error feeding into the recovery pipeline
is somewhere else entirely — most likely the embedded JS
bundle or early app init (DB download screen, etc.).
Two changes:
1. updates.enabled: false — disables expo-updates entirely
on this build, which removes the ErrorRecovery pipeline
from the startup path. If the app reaches the home screen,
expo-updates was the proximate cause. If it still crashes,
the underlying error is now visible in the new crash log
without the recovery machinery masking it.
2. version 1.0.6 → 1.0.7 — keeps TestFlight builds distinct
and invalidates the runtime namespace so no accidentally-
published future OTA can land on this binary during
diagnostic testing. Harmless either way.
This mirrors the pattern from commit 9f33448 (fix: disable
expo-updates to surface real launch crash + bump to 1.0.3),
which was used for the same reason during a previous crash
investigation.
Once the underlying cause is fixed, expo-updates should be
re-enabled and a production bundle published for the new
runtime version.
…eption)
## Root cause
Build 1.0.6(15) and 1.0.6(16) crashed on TestFlight ~13s after launch with a
signature that landed inside our own patched RCTTurboModule.mm:446 — i.e.
the iOS 26 patch from #1514 was doing its job re-throwing the NSException
cleanly, but something was still throwing an NSException from a void sync
TurboModule call that nothing up the GCD queue catches.
Stack traced to ContentUpdater.downloadWithProgress:
destFile.create({ overwrite: true });
destFile.write(bytes); // ~90 MB Uint8Array, sync Expo Modules Function
expo-file-system's File#write is exposed as a synchronous `Function`
(not AsyncFunction) on iOS. When invoked with a ~90 MB buffer, any
internal Swift error Data(bytes:count:).write(to:) raises — sandbox
scoped-access, disk pressure, TypedArray marshalling, etc. — surfaces as
an NSException via ObjCTurboModule::performVoidMethodInvocation. With
the #1514 patch in place the exception propagates cleanly through the
dispatch queue, but there is no @catch up the stack so the process
aborts instead of giving us a user-visible error.
The ContentUpdater docstring notes the 1.0.5 build hit exactly the same
crash signature against the legacy expo-file-system shim. Moving to the
new File API removed the shim but did not remove the single-shot sync
write pattern — same risk surface, same outcome.
## Fix
Replace the single destFile.write(bytes) call with a chunked write
through a FileHandle (destFile.open() → handle.writeBytes(chunk) loop →
handle.close() in finally), 1 MiB per chunk. This keeps each TurboModule
invocation in a size range that native file-system ops routinely handle.
Wrap the full write path in a try/catch so any failure becomes a promise
rejection surfaced by DbDownloadScreen's error UI as 'File write failed
after N bytes received: <message>' — no more silent process aborts, and
if this theory turns out to be wrong the next build will at least give
us the real error string.
## Tests
Extended the MockFile jest stub with open()/writeBytes/handleClose
tracking. Added three regression tests:
- New path uses FileHandle, not File#write
- 2.5 MiB payload splits into 3 chunks (1 MiB, 1 MiB, 0.5 MiB)
- Handle closes even when writeBytes throws; error surfaces cleanly
All 35 ContentUpdater tests pass. Full suite: 465 suites / 3431 tests
pass, zero TS errors, zero new lint errors.
## Scope
ContentUpdater.ts + its test file only. No app.json / eas.json / native
changes. updates.enabled stays false from #1516. Version stays 1.0.7 per
Craig's call — EAS autoIncrement handles the build number.
## References
- PR #1514 (iOS 26 TurboModule patch — successfully shipped)
- PR #1516 (diagnostic disable updates — shipped, same crash signature)
- react/react-native#54859 (TurboModule void-method NSException)
- SDK 54 expo-file-system File vs FileHandle API
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
TestFlight build 1.0.6(15) — the first build with PR #1514's iOS 26 TurboModule fix merged — launches, then crashes ~0.8s later. Unlike the original crash (which was in
ObjCTurboModule::performVoidMethodInvocation), this one fires inside expo-updates' deliberate recovery-pipeline abort:Reading the source: expo-updates runs
waitForRemoteUpdate → launchCached → crashwhen it catches an initial error. On a fresh install where the remote server has nothing matching and the cache is empty, the pipeline blows straight through all three steps. The crash is not the bug — it's the symptom. Something else throws the initial error into expo-updates, and then expo-updates abends the app deliberately because it can't recover.Ruled out
Stale OTA bundle mismatch. The EAS Updates dashboard shows all recent published updates tagged runtime
1.0.0. Our binary is runtime1.0.6. A runtime-version mismatch means the server returns "no update" to the device — it never attempts to load a bundle — so a bad bundle can't be the trigger.The plan
Temporarily disable expo-updates. This removes the
ErrorRecoverypipeline from the startup path entirely. Whatever throws the initial error is no longer caught and converted into a deliberateabort()— it either propagates into a more informative crash signature or the app launches successfully.Three possible outcomes on the next TestFlight build:
productionbundle for the new runtime version.ErrorRecovery.crash()signature → would be very surprising since that code path requiresenabled: trueto activate. Would force a close reading of config.Changes
Exactly two edits in
app/app.json:version1.0.61.0.7updates.enabledtruefalseEverything else untouched —
updates.urlpreserved, project ID preserved, runtime policy unchanged,runtimeVersion: { policy: "appVersion" }stays.Precedent
Commit
9f33448cdid exactly this ("fix: disable expo-updates to surface real launch crash + bump to 1.0.3") during an earlier crash investigation. Same diagnostic move; clean pattern in the repo.Post-merge
eas build --platform ios --profile production— source compile of React-Core is still in play via the#1514patchRollback
git revert+ rebuild. Harmless; only restores the old config.Follow-up (out of scope for this PR)
Once the real bug is fixed and the app launches, a follow-up PR should:
updates.enabled: trueeas update --branch productionto publish a bundle against runtime1.0.7I'll open a cleanup reminder issue tracking that.