Error Handling & Monitoring
TrickBook currently has no error boundaries, no global error handler, and no production error tracking. One uncaught error crashes the entire mobile app with a white screen.
Mobile: React Native Error Boundary
Root Error Boundary
Create a root error boundary that catches any unhandled error in the component tree:
// src/components/ErrorBoundary.tsx
import React, { Component, ErrorInfo, ReactNode } from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import * as Sentry from '@sentry/react-native';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
}
class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false, error: null };
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
// Report to Sentry
Sentry.captureException(error, { extra: { componentStack: errorInfo.componentStack } });
}
handleReset = () => {
this.setState({ hasError: false, error: null });
};
render() {
if (this.state.hasError) {
if (this.props.fallback) return this.props.fallback;
return (
<View style={styles.container}>
<Text style={styles.title}>Something went wrong</Text>
<Text style={styles.message}>The app ran into an unexpected error.</Text>
<TouchableOpacity style={styles.button} onPress={this.handleReset}>
<Text style={styles.buttonText}>Try Again</Text>
</TouchableOpacity>
</View>
);
}
return this.props.children;
}
}
Wrap Root Layout
// app/_layout.tsx
import { ErrorBoundary } from '@/components/ErrorBoundary';
export default function RootLayout() {
return (
<ErrorBoundary>
<Providers>
<Stack />
</Providers>
</ErrorBoundary>
);
}
Backend: Global Error Handler
Error Handler Middleware
Currently every route handles errors independently with try/catch. Add a centralized handler:
// middleware/errorHandler.js
const Sentry = require('@sentry/node');
const errorHandler = (err, req, res, next) => {
// Report to Sentry
Sentry.captureException(err);
// Log structured error
console.error({
message: err.message,
stack: err.stack,
method: req.method,
path: req.path,
userId: req.user?._id,
timestamp: new Date().toISOString(),
});
// Operational errors (expected)
if (err.isOperational) {
return res.status(err.statusCode || 400).json({
error: err.message,
});
}
// Programming errors (unexpected) - don't leak details
res.status(500).json({
error: 'Internal server error',
});
};
module.exports = errorHandler;
Register as Last Middleware
// index.js
const errorHandler = require('./middleware/errorHandler');
// ... all routes ...
// Must be AFTER all routes
app.use(errorHandler);
Custom Error Class
// utils/AppError.js
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = AppError;
Usage in routes:
const AppError = require('../utils/AppError');
router.get('/trick/:id', auth, async (req, res, next) => {
try {
const trick = await db.collection('tricks').findOne({ _id: id });
if (!trick) throw new AppError('Trick not found', 404);
res.json(trick);
} catch (error) {
next(error); // Passes to global error handler
}
});
Sentry Integration
Mobile Setup
cd TrickList
npx expo install @sentry/react-native
// app/_layout.tsx
import * as Sentry from '@sentry/react-native';
Sentry.init({
dsn: process.env.EXPO_PUBLIC_SENTRY_DSN,
environment: __DEV__ ? 'development' : 'production',
tracesSampleRate: 0.2,
enableAutoSessionTracking: true,
});
Backend Setup
cd Backend
npm install @sentry/node
// index.js (top of file, before other imports)
const Sentry = require('@sentry/node');
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV || 'development',
tracesSampleRate: 0.2,
});
// After Express app is created
Sentry.setupExpressErrorHandler(app);
What Sentry Captures
| Event | Mobile | Backend |
|---|---|---|
| Unhandled exceptions | Yes | Yes |
| Unhandled promise rejections | Yes | Yes |
| HTTP errors (4xx, 5xx) | Via interceptor | Via middleware |
| Slow transactions | Yes (traces) | Yes (traces) |
| User context | Attached via Sentry.setUser() | Attached via middleware |
| Release tracking | Via EAS build | Via deploy tag |
Sentry Environment Variables
Add to .env.example for both repos:
# TrickList
EXPO_PUBLIC_SENTRY_DSN=https://xxx@o123.ingest.sentry.io/456
# Backend
SENTRY_DSN=https://xxx@o123.ingest.sentry.io/789
Backend: Graceful Shutdown
The backend currently has no shutdown handling. Abrupt stops can corrupt WebSocket connections and leave database operations incomplete.
// index.js
const { closeConnection } = require('./db');
const gracefulShutdown = async (signal) => {
console.log(`Received ${signal}. Starting graceful shutdown...`);
// Stop accepting new connections
server.close(async () => {
console.log('HTTP server closed');
// Close Socket.IO connections
io.close();
console.log('Socket.IO closed');
// Close database connection
await closeConnection();
console.log('Database connection closed');
process.exit(0);
});
// Force shutdown after 10 seconds
setTimeout(() => {
console.error('Forced shutdown after timeout');
process.exit(1);
}, 10000);
};
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
Backend: Health Check Endpoint
Add a health check for load balancers and monitoring:
// routes/health.js
router.get('/health', async (req, res) => {
try {
// Check database connectivity
const db = await getDb();
await db.command({ ping: 1 });
res.json({
status: 'healthy',
uptime: process.uptime(),
timestamp: new Date().toISOString(),
});
} catch (error) {
res.status(503).json({
status: 'unhealthy',
error: 'Database connection failed',
});
}
});