●TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13●OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude Code●SIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App Store●MAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessage●NATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core ML●SEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round●TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13●OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude Code●SIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App Store●MAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessage●NATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core ML●SEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round
Advanced Error Handling, Debugging & Production Monitoring in Rork Max — Building Bulletproof Apps
Practical patterns for bringing production-grade resilience to Rork Max apps: layered Error Boundaries, global error handlers, structured logging, and Sentry/Crashlytics integration — plus delta-based monitoring and log-sampling lessons from running several apps in production.
Setup and context — Why Error Handling and Monitoring Matter
Building features is only half the battle. When a production app crashes, you lose user trust, receive negative App Store reviews, and risk damaging your brand. Rork Max makes building apps remarkably fast, but ensuring robustness through proper error handling and monitoring is something you need to architect yourself.
These are the error handling, debugging, and production monitoring patterns I apply to my own Rork Max apps, paired with the adjustments that actually moved the numbers in production. Built on the React Native (Expo) ecosystem, these patterns cover everything from initial development through post-release operations.
Target audience: Developers who have shipped at least one Rork app, or intermediate-to-advanced developers preparing for production deployment.
Prerequisites: Familiarity with React Native / Expo basics, TypeScript fundamentals, and Rork Max core features.
Error Boundaries for UI-Level Fault Isolation
Rendering errors in React's component tree can crash your entire app. By strategically placing Error Boundaries, you can contain failures and minimize their blast radius.
Rather than relying on a single Error Boundary, the advanced pattern involves placing them at multiple layers of your app hierarchy.
// app/_layout.tsx — Root-level Error Boundaryimport ErrorBoundary from '@/components/ErrorBoundary';import { reportToSentry } from '@/utils/errorReporter';export default function RootLayout() { return ( <ErrorBoundary onError={reportToSentry} fallback={<CriticalErrorScreen />}> <NavigationContainer> <ErrorBoundary onError={reportToSentry}> {/* Screen-level errors are caught individually */} <MainStack /> </ErrorBoundary> </NavigationContainer> </ErrorBoundary> );}
In this design, the root-level Error Boundary catches navigation infrastructure failures, while screen-level boundaries handle individual screen rendering errors. Users can retry at the screen level without the entire app crashing.
✦
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
✦Monitoring crash-free rate by delta rather than absolute value to catch regressions half a day sooner
✦Sampling remote logs per user instead of per error to cut transmission volume to roughly 1/8
✦Using beforeSend to separate non-actionable errors, cutting actionable alerts from ~70 to ~12 per month
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.
React Error Boundaries only catch errors during the rendering phase. Async operations, event handlers, and native module errors require a different approach.
Catching Unhandled Promise Rejections
// utils/globalErrorHandler.tsimport { logger } from './logger';export function setupGlobalErrorHandlers() { // Unhandled Promise rejections const originalHandler = global.ErrorUtils?.getGlobalHandler(); global.ErrorUtils?.setGlobalHandler((error: Error, isFatal: boolean) => { logger.error('GlobalHandler', { message: error.message, stack: error.stack, isFatal, }); // For fatal errors, notify the user if (isFatal) { // Send crash report before delegating to default handler sendCrashReport(error).finally(() => { originalHandler?.(error, isFatal); }); } }); // unhandledrejection may require a polyfill in React Native if (typeof global.addEventListener === 'function') { global.addEventListener('unhandledrejection', (event: any) => { logger.warn('UnhandledRejection', { reason: event?.reason?.message || String(event?.reason), }); }); }}async function sendCrashReport(error: Error): Promise<void> { try { await fetch('https://your-api.example.com/crash-report', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: error.message, stack: error.stack, timestamp: new Date().toISOString(), platform: require('react-native').Platform.OS, }), }); } catch { // Swallow errors from the crash reporter itself }}
Registering Handlers at App Startup
// App.tsx or app/_layout.tsximport { setupGlobalErrorHandlers } from '@/utils/globalErrorHandler';// Call immediately on app startupsetupGlobalErrorHandlers();
This ensures that async errors and native-layer crashes that Error Boundaries can't catch are still reliably logged.
Designing and Implementing a Custom Logger
Relying solely on console.log won't help you track down issues in production. A structured logger that can optionally forward logs to a remote service is essential.
This logger outputs detailed logs to the console during development and sends only error-level logs to a remote endpoint in production. Buffering keeps network overhead minimal.
Integrating Sentry for Production Error Monitoring
To efficiently detect and analyze production errors, a dedicated monitoring service is essential. Sentry has excellent compatibility with React Native / Expo and integrates smoothly with Rork Max apps.
Setting Up Sentry
# Install Sentry in your Expo projectnpx expo install @sentry/react-native
// utils/sentry.tsimport * as Sentry from '@sentry/react-native';import Constants from 'expo-constants';export function initSentry() { Sentry.init({ dsn: 'YOUR_SENTRY_DSN', // Get this from your Sentry dashboard environment: __DEV__ ? 'development' : 'production', release: Constants.expoConfig?.version || '1.0.0', dist: Constants.expoConfig?.ios?.buildNumber || Constants.expoConfig?.android?.versionCode?.toString() || '1', // Performance monitoring (sampling rate) tracesSampleRate: __DEV__ ? 1.0 : 0.2, // Session replay (record only on errors) replaysSessionSampleRate: 0, replaysOnErrorSampleRate: 1.0, beforeSend(event) { // Don't send events in development if (__DEV__) return null; // Filter sensitive information if (event.request?.headers) { delete event.request.headers['Authorization']; } return event; }, });}// Helper for sending error reportsexport function reportToSentry(error: Error, context?: Record<string, unknown>) { Sentry.withScope((scope) => { if (context) { Object.entries(context).forEach(([key, value]) => { scope.setExtra(key, value); }); } Sentry.captureException(error); });}// Set user info (call on login)export function setSentryUser(userId: string, email?: string) { Sentry.setUser({ id: userId, email });}// Navigation trackingexport function trackNavigation(routeName: string) { Sentry.addBreadcrumb({ category: 'navigation', message: `Navigated to ${routeName}`, level: 'info', });}
Connecting with Error Boundaries
// Integrate Sentry with the ErrorBoundary from earlier<ErrorBoundary onError={(error, errorInfo) => { Sentry.withScope((scope) => { scope.setExtra('componentStack', errorInfo.componentStack); scope.setTag('errorBoundary', 'screen-level'); Sentry.captureException(error); }); }}> <ScreenContent /></ErrorBoundary>
API Communication Error Handling Patterns
Network communication is the most error-prone area of any app. Building robust error handling into your API client layer reduces the burden on individual screen components.
This client includes exponential backoff retry logic, timeout control, and differentiation between client and server errors. Every retry is logged, and final failures are reported to Sentry.
Managing Error State with React Hooks
Here's a reusable Hook for managing async operation state and surfacing errors in your UI.
Alongside production monitoring, efficient debugging during development is equally important. Here are techniques you can leverage in your Rork Max + Expo development workflow.
Leveraging Flipper and React Native Debugger
For React Native debugging, tools like Flipper and React Native Debugger let you inspect network requests, Redux store changes, and layout details in real time.
// flipper-plugin.ts (enabled only in development)if (__DEV__) { // Enable Flipper's network plugin require('react-native-flipper').addPlugin({ getId() { return 'CustomNetworkLogger'; }, onConnect(connection: any) { // Set up API request interception logger.info('Flipper', 'Connected to Flipper'); }, onDisconnect() { logger.info('Flipper', 'Disconnected from Flipper'); }, });}
Dual Integration: Firebase Crashlytics with Sentry
Using Sentry and Firebase Crashlytics together gives you broader error coverage. Sentry excels at detailed stack traces and context (breadcrumbs, custom tags), while Crashlytics offers tight integration with the Firebase ecosystem.
// utils/crashReporter.tsimport * as Sentry from '@sentry/react-native';import crashlytics from '@react-native-firebase/crashlytics';import { logger } from './logger';export const crashReporter = { // Send non-fatal errors to both services recordError(error: Error, context?: Record<string, string>) { // Sentry Sentry.withScope((scope) => { if (context) { Object.entries(context).forEach(([k, v]) => scope.setTag(k, v)); } Sentry.captureException(error); }); // Firebase Crashlytics if (context) { Object.entries(context).forEach(([k, v]) => { crashlytics().setAttribute(k, v); }); } crashlytics().recordError(error); logger.error('CrashReporter', { message: error.message, ...context, }); }, // Log to both services log(message: string) { Sentry.addBreadcrumb({ message, level: 'info' }); crashlytics().log(message); }, // Set user identity in both services setUser(userId: string) { Sentry.setUser({ id: userId }); crashlytics().setUserId(userId); },};
Production Health Checks and Alert Design
Setting up monitoring tools is just the beginning. How you design your alerts determines how quickly you can respond to incidents.
Alert Rule Design Guidelines
When configuring Sentry alerts, prioritize based on these criteria:
P0 (immediate response): Crash rate exceeds 1%, or more than 10 crashes occur within 1 hour of a new version release
P1 (respond within 4 hours): Error rate for a specific API endpoint exceeds 5%
P2 (next business day): A new type of error is detected for the first time
Post-Release Canary Checks
// utils/canaryCheck.tsimport { logger } from './logger';import Constants from 'expo-constants';export async function runCanaryChecks(): Promise<void> { const checks = [ { name: 'API Health', fn: checkApiHealth }, { name: 'Auth Token Valid', fn: checkAuthToken }, { name: 'Storage Accessible', fn: checkStorage }, ]; for (const check of checks) { try { await check.fn(); logger.info('Canary', `${check.name}: OK`); } catch (error) { logger.error('Canary', { check: check.name, error: (error as Error).message, appVersion: Constants.expoConfig?.version, }); } }}async function checkApiHealth(): Promise<void> { const res = await fetch('https://api.example.com/health'); if (!res.ok) throw new Error(`API returned ${res.status}`);}async function checkAuthToken(): Promise<void> { // Read token from AsyncStorage and validate format // Attempt early refresh if expired}async function checkStorage(): Promise<void> { // Test AsyncStorage read/write operations}
Running these canary checks in the background at app startup lets you detect problems before they impact users.
What the Docs Won't Tell You — Lessons from Production
The Sentry and Crashlytics docs cover the APIs well, but they don't cover what you learn from running several apps over many years. I've shipped iOS and Android apps as a solo developer since 2014 — quiet utilities in the wallpaper, calming-tone, and intention space — with around 50 million cumulative downloads. The more apps I shipped, the more I realized that knowing exactly what is happening matters more for day-to-day operations than simply reducing crash counts.
My grandfather, a traditional shrine carpenter, used to say that working with your hands is itself a form of devotion. Error monitoring is similar: it isn't the flashy features that keep an app healthy in production, it's the unglamorous habit of looking at your logs every day. Below are the adjustments that actually moved the numbers for me.
1. Read Crash-Free Rate as a Delta, Not an Absolute
When you look at crash-free users in the Crashlytics dashboard, judging by the absolute value alone leads you astray. My wallpaper apps held a steady crash-free rate of 99.7–99.8% in normal conditions. Treating "99.7% is great" as reassuring is dangerous. What matters is the delta around a release: when a new version dropped from 99.8% to 99.2% within 24 hours of going live, the absolute number was still high, yet the number of affected users had swelled to roughly 4× the usual.
So in addition to Crashlytics Velocity Alerts, I compute the difference between the last 24 hours and the 7-day median from my own logs, and notify on any regression of 0.3 percentage points or more. After adopting this, I caught release-induced regressions about half a day earlier on average — early enough to ship a fix before App Store ratings took a hit.
// utils/crashRateMonitor.ts// Detect release-induced regressions via delta from the normal medianinterface CrashSample { date: string; // YYYY-MM-DD crashFreeRate: number; // 0–1 (e.g., 0.997)}export function detectRegression( recent: CrashSample, baseline: CrashSample[] // last 7 days): { regressed: boolean; deltaPoint: number } { const sorted = [...baseline].map((s) => s.crashFreeRate).sort((a, b) => a - b); const median = sorted[Math.floor(sorted.length / 2)] ?? recent.crashFreeRate; // difference in percentage points const deltaPoint = (median - recent.crashFreeRate) * 100; return { regressed: deltaPoint >= 0.3, deltaPoint };}
An absolute threshold (e.g., notify if it falls below 99.5%) tends to mean "by the time you notice, it's too late" — especially for apps whose baseline quality is high. Delta-based monitoring is practical because it anchors on your app's normal state.
2. Sample Remote Logs by User, Not by Error
Sending every log remotely incurs non-trivial network and storage costs. Initially I buffered and sent all errors, like the Logger shown earlier. But when a popular app hits the same bug at once, tens of thousands of identical stack traces flood in, burying the rare-but-serious errors that actually matter.
After the fix, instead of sampling per error type, I sample 10% of users and, for users outside the sample, send only aggregated counts by error type. This cut transmission volume to roughly 1/8 while still catching every new class of error.
// utils/logSampler.ts// Consistently include or exclude the same user from samplingfunction hashToUnit(userId: string): number { let h = 0; for (let i = 0; i < userId.length; i++) { h = (h * 31 + userId.charCodeAt(i)) | 0; } return (Math.abs(h) % 1000) / 1000; // 0–1}export function shouldSendFullLog(userId: string, sampleRate = 0.1): boolean { // Consistent per-user decision (same user always gets the same result) return hashToUnit(userId) < sampleRate;}
The key insight: thinning by individual error fragments a single user's error timeline, making reproduction hard. Thinning by user keeps the complete log for the users you do sample — far more useful for reproducing failures.
3. Use beforeSend to Separate "Not My Fault" Errors
The first thing that surprised me after putting Sentry into production was the volume of errors unrelated to my own code: Network request failed from dropped connections, exhausted device storage, WebView crashes on old OS versions. Left alone, these bury your dashboard in noise and hide the errors you should actually fix.
In beforeSend, I route unrecoverable, out-of-my-control errors to a separate tag and exclude them from alerts. My monthly count of "errors worth acting on" dropped from about 70 notifications before the filter to around 12 — and with more time per issue, the quality of my fixes went up.
// Filter to add to Sentry's beforeSendconst NON_ACTIONABLE_PATTERNS = [ /Network request failed/i, /The request timed out/i, /No space left on device/i,];beforeSend(event, hint) { if (__DEV__) return null; const msg = hint?.originalException instanceof Error ? hint.originalException.message : event.message ?? ''; if (NON_ACTIONABLE_PATTERNS.some((re) => re.test(msg))) { // Don't drop it — keep it under a separate tag for aggregation, just off alerts event.tags = { ...event.tags, actionable: 'false' }; event.fingerprint = ['non-actionable', msg.slice(0, 40)]; } return event;}
The trick is to keep them under a separate tag rather than dropping them. If you later notice a spike in network-disconnect errors, that becomes a signal to revisit your retry design.
A Pre-Release Operations Checklist
Finally, here's what I always verify before publishing an app to the store. It isn't about code completeness — it's about confirming I'll notice whatever happens after launch.
Confirmed on a real device that fatal errors are caught by global.ErrorUtils and a crash report is sent
Sentry's release and dist (build number) match the build I'm shipping to the store (a mismatch breaks symbolication and leaves stack traces unreadable)
beforeSend reliably strips sensitive data such as the Authorization header
The crash-free-rate "delta alert" is enabled, and I've sent a test to confirm it actually reaches my channel (email/Slack)
I've blocked out time to actively watch the dashboard for the first hour after release
Number five is a mindset rather than a mechanism, but it helps most. A release isn't done when you ship it — I treat the first hour as the real peak of the work.
Summary
Error handling and production monitoring are critical areas that directly impact your app's quality and operational efficiency. By combining the multi-layered Error Boundary patterns, global error handlers, structured loggers, and Sentry integration covered in this article, you can bring production-grade robustness to your Rork Max apps.
Start with Error Boundary placement and global error handler setup, then add Sentry or Crashlytics as your app grows. No app is completely error-free, but an app that detects errors quickly and responds accurately is absolutely achievable.
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.