RORK LABEN
TOOLING — Rorkの開発者向けリポジトリが動き続けています。rork-xcodeが7月16日、rork-deviceが7月15日、rork-plistが7月13日に更新されましたOPUS46 — RorkでClaude Opus 4.6が稼働しています。Rork MaxはClaude Codeを土台にアプリを組み立てる設計ですSIM — ブラウザ上で動くクラウドのiOSシミュレータを備え、実機へのインストールは1クリック、App Storeへの提出は2クリックと案内されていますMAX — Rork MaxはReact Nativeではなく純粋なSwiftを出力します。iPhone・iPad・Apple Watch・Apple TV・Vision Pro、そしてiMessageまでが射程ですNATIVE — HealthKit、ARKitとLiDAR、NFC、Dynamic Island、Live Activities、Metalによる3D、Core MLのオンデバイス推論まで扱えますSEED — RorkはLeft Lane Capitalが主導する1,500万ドルのシードラウンドを実施し、Peak XVとa16z Speedrunが参加しましたTOOLING — Rorkの開発者向けリポジトリが動き続けています。rork-xcodeが7月16日、rork-deviceが7月15日、rork-plistが7月13日に更新されましたOPUS46 — RorkでClaude Opus 4.6が稼働しています。Rork MaxはClaude Codeを土台にアプリを組み立てる設計ですSIM — ブラウザ上で動くクラウドのiOSシミュレータを備え、実機へのインストールは1クリック、App Storeへの提出は2クリックと案内されていますMAX — Rork MaxはReact Nativeではなく純粋なSwiftを出力します。iPhone・iPad・Apple Watch・Apple TV・Vision Pro、そしてiMessageまでが射程ですNATIVE — HealthKit、ARKitとLiDAR、NFC、Dynamic Island、Live Activities、Metalによる3D、Core MLのオンデバイス推論まで扱えますSEED — RorkはLeft Lane Capitalが主導する1,500万ドルのシードラウンドを実施し、Peak XVとa16z Speedrunが参加しました
記事一覧/開発ツール
開発ツール/2026-05-07中級

Rork で作ったアプリの「原因不明クラッシュ」を撲滅する — Error Boundary と Unhandled Promise の実戦的な捕捉法

Rork が生成したコードに頻出する「await のエラー握り潰し」と「ErrorBoundary なし」の組み合わせが原因で、本番アプリが原因不明で落ちる問題。実装で解決する手順を解説します。

Rork515Error Boundary2React Native209Expo149クラッシュ対策4Sentry6

Rork で生成したアプリを TestFlight に出した直後、Crashlytics に「JavaScriptError: undefined is not an object」とだけ書かれたクラッシュが、ユーザー数十人ぶん毎日積み上がっていったことがあります。スタックトレースは <anonymous> の連続で、自分が書いた行は一切見当たりません。再現条件も分からず、3日ほど胃を痛めました。

原因を突き止めて分かったのは、Rork が生成するコードの傾向と、React Native のエラー伝播モデルが噛み合わないという構造的な問題でした。?. で安全アクセスは徹底されているのに、await した非同期関数の中で投げられた例外はどこにも捕捉されておらず、結果として「JS スレッドが silently 死ぬ」状態が量産されていたのです。

ここではRork で作ったアプリ特有のクラッシュパターンと、本番運用に耐える Error Boundary・Unhandled Promise Rejection 捕捉の最小構成を、コピペで使える形で残しておきます。AI 生成コードを商用に持っていく方の参考になれば幸いです。

なぜ Rork 生成コードはサイレントクラッシュを起こしやすいのか

Rork は React Native + Expo のコードを生成しますが、画面遷移の構造や API 呼び出しのパターンには、AI 特有の偏りが見られます。具体的には次のような傾向です。

  • try/catch よりも ?.?? で防御する書き方が多い(ランタイム例外を「投げる前に握り潰す」発想)
  • 非同期関数を useEffect 内で await ではなく即時実行する書き方が多い(fire-and-forget)
  • エラー時のフォールバック UI が、画面コンポーネント内ではなくテキスト表示に閉じている

この3つが重なると、ネットワークが不安定な実機では「画面が真っ白のまま固まる」「次の画面に遷移できない」「ボタンを押しても何も起きない」という症状が起きます。Crashlytics には何も上がらないか、上がっても役に立たない情報しか出ないことが多く、ユーザーは黙ってアプリを削除してしまいます。

私の運営しているアプリ事業では、累計 5,000 万ダウンロードのうち、こうした「サイレントクラッシュ」が一番のレビュー★1 量産機でした。逆に言えば、ここを潰せばレビュー評価が目に見えて改善します。エラーハンドリング全般の基本から押さえたい方は、先に Rork アプリのエラーハンドリングとクラッシュ対策入門 に目を通しておくと、この記事の内容がより腹落ちすると思います。

Error Boundary は「最後の砦」で、最初に置くもの

React Native には標準で Error Boundary 相当の仕組みがありません。React.ComponentcomponentDidCatch を実装したクラスコンポーネントを自分で用意するか、react-error-boundary パッケージを使うことになります。

2026年時点の現場感覚では、react-error-boundary の利用が明確に楽です。関数コンポーネントで書けますし、リセット機構も整っています。

// app/_layout.tsx の最上位に置く想定
import { ErrorBoundary } from 'react-error-boundary';
import { Stack } from 'expo-router';
import { Text, View, Pressable, StyleSheet } from 'react-native';
 
function FallbackScreen({ error, resetErrorBoundary }: {
  error: Error;
  resetErrorBoundary: () => void;
}) {
  return (
    <View style={styles.container}>
      <Text style={styles.title}>申し訳ありません、問題が発生しました</Text>
      <Text style={styles.message}>{error.message}</Text>
      <Pressable style={styles.button} onPress={resetErrorBoundary}>
        <Text style={styles.buttonText}>もう一度試す</Text>
      </Pressable>
    </View>
  );
}
 
export default function RootLayout() {
  return (
    <ErrorBoundary
      FallbackComponent={FallbackScreen}
      onError={(error, info) => {
        // ここで Sentry/Crashlytics へ送る
        console.error('[ErrorBoundary]', error, info.componentStack);
      }}
    >
      <Stack />
    </ErrorBoundary>
  );
}
 
const styles = StyleSheet.create({
  container: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 24 },
  title: { fontSize: 18, fontWeight: '600', marginBottom: 12 },
  message: { fontSize: 14, color: '#666', marginBottom: 24, textAlign: 'center' },
  button: { backgroundColor: '#000', paddingHorizontal: 24, paddingVertical: 12, borderRadius: 8 },
  buttonText: { color: '#fff', fontWeight: '600' },
});
 
// 期待する挙動: 子ツリー内のレンダーエラーを必ず捕捉し、ユーザーに「もう一度試す」を提示する
// resetErrorBoundary を呼ぶと再マウントされ、状態が初期化される

ポイントは Stack 全体を1つの ErrorBoundary でラップしている点です。expo-router を使っていると、ルートごとに try してしまいがちですが、ルート画面の遷移そのもので落ちる場合があるため、最上位に置く方が捕捉率が高くなります。

onError のコールバックでログ送信を行う設計にしておくと、後で Sentry に差し替える際に呼び出し箇所を変えずに済みます。

expo-router v3+ なら ErrorBoundary export も併用できる

expo-router は v3 から、各ルートファイルに ErrorBoundary を named export することで、そのルート配下のレンダーエラーを個別に拾えるようになりました。

// app/(tabs)/profile.tsx
export { ErrorBoundary } from 'expo-router';
// 必要なら自前のコンポーネントも書ける

最上位の react-error-boundary で全体を守りつつ、ユーザー体験的にリセットしやすい画面(プロフィール編集など)にだけ個別の ErrorBoundary を置く、という二段構えが現実的です。

Unhandled Promise Rejection は ErrorBoundary では拾えない

ここが Rork 生成コード対策の肝です。Error Boundary はレンダー中に投げられた例外しか捕まえられません。 つまり、useEffect の中で発火した非同期処理の中で投げられた例外は、Error Boundary を素通りしてしまいます。

// これは ErrorBoundary に拾われない
useEffect(() => {
  fetch('/api/me')
    .then(r => r.json())
    .then(data => setUser(data.user.profile.name)); // user が null なら TypeError
}, []);

React Native では、こうした unhandled promise rejection は警告ログには出るものの、本番ビルドでは LogBox が無効化されているため、ユーザーには何も表示されないまま JS の状態だけが壊れます。

これを拾うには、グローバルな unhandledrejection ハンドラと、React Native 標準の ErrorUtils を組み合わせます。

// utils/installGlobalErrorHandlers.ts
import { Alert, Platform } from 'react-native';
 
type ErrorReporter = (error: unknown, isFatal: boolean) => void;
 
export function installGlobalErrorHandlers(report: ErrorReporter) {
  // 1) 同期的な未捕捉エラー(JS スレッドの致命的エラー)
  const defaultHandler = ErrorUtils.getGlobalHandler();
  ErrorUtils.setGlobalHandler((error, isFatal) => {
    report(error, !!isFatal);
    defaultHandler(error, isFatal);
  });
 
  // 2) Promise の未捕捉 reject
  const tracking = require('promise/setimmediate/rejection-tracking');
  tracking.enable({
    allRejections: true,
    onUnhandled: (id: number, error: unknown) => {
      report(error, false);
    },
    onHandled: () => {
      // 後から catch されたケース(無視で OK)
    },
  });
 
  if (__DEV__ && Platform.OS !== 'web') {
    // 開発時はトーストでも気づけるようにしておく
    // 本番では Alert を出さない(UX を壊すため)
  }
}
 
// app/_layout.tsx の冒頭で1回だけ呼ぶ
// installGlobalErrorHandlers((err, fatal) => {
//   sendToSentry(err, { fatal });
// });

ErrorUtils は React Native の global に存在しますが TypeScript 型が用意されていないため、グローバル宣言を追加するか (global as any).ErrorUtils で参照することになります。promise/setimmediate/rejection-tracking は React Native が内部的に使っているライブラリで、追加インストールは不要です。

私のチームでは、このグローバルハンドラを入れた直後の週に、Sentry に上がってくるエラー件数が3倍に増えました。それまで「届いていなかった」エラーが、ようやく可視化された瞬間でした。最初は数字を見て焦りますが、これが本来の状態です。

Sentry と Crashlytics、Rork アプリならどっち

両方使えるなら両方が理想ですが、リソースが限られている個人開発では、私は Sentry を先に入れることをお勧めしています。理由はシンプルで、JavaScript レイヤのエラーを構造化して送るには Sentry の方が一日の長があるからです。

// app/_layout.tsx
import * as Sentry from '@sentry/react-native';
 
Sentry.init({
  dsn: 'https://YOUR_SENTRY_DSN@o0.ingest.sentry.io/0',
  enableNative: true,
  enableNativeCrashHandling: true,
  // Rork が出力する .map ファイルを EAS でアップロードしておくとシンボル化される
  tracesSampleRate: 0.1,
  beforeSend(event, hint) {
    // 個人情報を含みそうなフィールドはここで落とす
    if (event.request?.headers) delete event.request.headers['Authorization'];
    return event;
  },
});
 
// グローバルハンドラに渡すレポーター
const report = (error: unknown, isFatal: boolean) => {
  Sentry.captureException(error, { tags: { isFatal: String(isFatal) } });
};

Sentry の enableNative: true を有効にしておけば、Hermes 由来のネイティブクラッシュも拾えます。Rork は Hermes 前提でビルドされるため、ここを切ると JS 側の例外しか拾えなくなり、原因不明クラッシュの半分を見逃します。

Crashlytics は Android のネイティブ・クラッシュ集計に強いので、規模が大きくなってきたら追加する、という順序が落ち着きます。Sentry のセットアップ手順は Rork × Sentry でクラッシュ監視を導入する手順 にまとめてあるので、本格的に組み込むタイミングで参照してください。

押さえておきたい3つの実装上の落とし穴

実装してみて初めて気づくポイントが3つあります。先に書いておきます。

  1. __DEV__ 中は ErrorBoundary を意図的に通り抜けさせるオプションがあります。 react-error-boundaryonError を実装したからといって、開発時の赤画面(LogBox)は無効化されません。本番ビルドでのみ ErrorBoundary が働く感覚に近いので、TestFlight でテストする際は必ずリリースビルドで確認してください。
  2. onError 内でさらに throw すると ErrorBoundary が再帰します。 Sentry 送信に失敗した場合に throw すると、ErrorBoundary 自身がエラーになって fallback が表示されない事故を起こします。try/catch で囲むか、captureException のオプションで送信失敗を握り潰してください。
  3. resetErrorBoundary だけでは状態は元に戻らありません。 Error Boundary は React のコンポーネントツリーをアンマウント→再マウントするだけなので、Zustand や Jotai のストアに残った壊れた状態は持ち越されます。リセットボタンの中でストアの clear も呼ぶ設計にしておくと、ユーザーが本当に「やり直せた」状態になります。

私自身、3 番目の落とし穴で半日溶かしました。ストアに残った null データを参照し続けて、ErrorBoundary がリセット後に再び発火する無限ループになっていたのです。

仕掛けがちゃんと働いているか確認する隠しルート

本番に出す前に、どの層が捕まえているかを目で確認できる小さな仕掛けを入れておくと安心です。私は環境変数で開発時にだけ表示される隠しルートを毎プロジェクトに置いています。

// app/_dev/error-test.tsx — EXPO_PUBLIC_DEV_TOOLS=1 のときだけルートに登録
import { Pressable, View, Text } from 'react-native';
 
export default function ErrorTest() {
  return (
    <View>
      <Pressable onPress={() => { throw new Error('render-time test'); }}>
        <Text>レンダー中に投げる</Text>
      </Pressable>
      <Pressable onPress={async () => { await Promise.reject(new Error('async test')); }}>
        <Text>Promise を reject</Text>
      </Pressable>
      <Pressable onPress={() => { setTimeout(() => { throw new Error('timer test'); }, 0); }}>
        <Text>setTimeout 内で投げる</Text>
      </Pressable>
    </View>
  );
}

リリースビルドで3つのボタンをそれぞれ押すと、1つ目は ErrorBoundary が、2つ目は Promise rejection ハンドラが、3つ目は ErrorUtils のグローバルハンドラが捕まえるはずです。どれか1つでも黙って通り抜けたら、配線が抜けている証拠です。15分のチェックを習慣にすると、計測が抜けたまま本番に出す事故が減ります。

まずは最上位の _layout.tsx に react-error-boundary を1つ置いてみてください

完全な構成を一気に入れる必要はありません。まずは react-error-boundary をインストールして、app/_layout.tsx<Stack /><ErrorBoundary> で包む。その1コミットだけでも、ユーザーが目にする「真っ白な画面のまま戻れない」体験は確実に減ります。

Unhandled Promise の捕捉やSentry連携は、その次の週末に取り組めば十分です。すでにアプリが白画面で固まっている症状がある方は、原因切り分けのフローを Rork で起きる白画面クラッシュのデバッグガイド にまとめてあるので、合わせて読んでみてください。React Native のエラー伝播はクセがあるぶん、一度コードに住まわせてしまえば、その後は同じパターンを使い回せます。お読みいただきありがとうございました。AI 生成のコードを商用品質まで磨き上げる過程で、同じ壁に当たっている方の参考になれば幸いです。

シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

開発ツール2026-06-22
壊れたキャッシュで毎回起動時に落ちるアプリ——ユーザーが自力で抜け出せる「セーフモード起動」を設計する
永続化したキャッシュが壊れて起動のたびに同じ場所で落ちると、ユーザーには再インストールしか残りません。アプリ自身が早期クラッシュの連続を数え、対話可能になって初めて起動を確定し、危険な状態だけを段階的にリセットするセーフモードを、Expo(React Native)の実装で設計します。
開発ツール2026-07-15
「、」が行頭に落ちた — Rork が組んだ名言カードと日本語の禁則処理
React Native の Text は日本語の行頭禁則を保証しません。実測した違反率、WORD JOINER による事前結合の実装、onTextLayout での自動監査、Rork Max(SwiftUI)との挙動差までまとめました。
開発ツール2026-07-11
Sentry のイベント枠を数日で使い切ったとき — 送る前にノイズを削って本当のエラーを残す運用メモ
Sentry のイベント枠が月初で尽きる原因はノイズの偏りです。beforeSend・サンプリング・グルーピングで送信前に量を絞り、重要なエラーだけを枠内に残す実装と、実際に送信量を測って立て直した運用手順をまとめました。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →