Skip to content

Commit c906f36

Browse files
authored
fix(core): implement global error handling and React ErrorBoundary (#288)
* fix(core): implement process-level error hooks and global exception handlers Resolves #282 - Add unhandledRejection and uncaughtException hooks to both Node.js API servers - Add React ErrorBoundary to web-dashboard - Add FastAPI global exception handler to python-service * fix(core): refactor process signal/error handlers to register before startup As per CodeRabbit review: - Register SIGTERM, SIGINT, unhandledRejection, and uncaughtException before starting the database or listening on the port. - Modify unhandledRejection and uncaughtException to call the graceful shutdown sequence instead of just logging or hard-exiting.
1 parent 92b871f commit c906f36

5 files changed

Lines changed: 138 additions & 41 deletions

File tree

apps/dashboard-api/src/app.js

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -172,28 +172,39 @@ if (process.env.NODE_ENV !== 'test') {
172172

173173
const { connectDB } = require('@urbackend/common');
174174

175-
// Start DB & Server
176-
connectDB();
177-
178-
const server = app.listen(PORT, () => {
179-
console.log(`Server running on port ${PORT}`);
180-
});
175+
let shuttingDown = false;
176+
let server;
181177

182178
// SHUTDOWN
183-
const gracefulShutdown = async () => {
184-
console.log('🛑 SIGTERM/SIGINT received. Shutting down gracefully...');
185-
186-
server.close(async () => {
187-
console.log('✅ HTTP server closed.');
179+
const gracefulShutdown = async (err = null) => {
180+
if (shuttingDown) return;
181+
shuttingDown = true;
182+
console.log('🛑 SIGTERM/SIGINT/Fatal Error received. Shutting down gracefully...');
183+
184+
if (err) {
185+
console.error('Fatal Error causing shutdown:', err);
186+
}
187+
188+
if (server) {
189+
server.close(async () => {
190+
console.log('✅ HTTP server closed.');
191+
try {
192+
await mongoose.connection.close(false);
193+
console.log('✅ MongoDB connection closed.');
194+
process.exit(err ? 1 : 0);
195+
} catch (dbErr) {
196+
console.error('❌ Error closing MongoDB connection:', dbErr);
197+
process.exit(1);
198+
}
199+
});
200+
} else {
188201
try {
189202
await mongoose.connection.close(false);
190-
console.log('✅ MongoDB connection closed.');
191-
process.exit(0);
192-
} catch (err) {
193-
console.error('❌ Error closing MongoDB connection:', err);
203+
process.exit(err ? 1 : 0);
204+
} catch (dbErr) {
194205
process.exit(1);
195206
}
196-
});
207+
}
197208

198209
// Force close after 10s
199210
setTimeout(() => {
@@ -202,8 +213,25 @@ if (process.env.NODE_ENV !== 'test') {
202213
}, 10000);
203214
};
204215

205-
process.on('SIGTERM', gracefulShutdown);
206-
process.on('SIGINT', gracefulShutdown);
216+
process.on('SIGTERM', () => gracefulShutdown());
217+
process.on('SIGINT', () => gracefulShutdown());
218+
219+
process.on('unhandledRejection', (reason, promise) => {
220+
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
221+
gracefulShutdown(reason);
222+
});
223+
224+
process.on('uncaughtException', (err) => {
225+
console.error('Uncaught Exception:', err);
226+
gracefulShutdown(err);
227+
});
228+
229+
// Start DB & Server
230+
connectDB();
231+
232+
server = app.listen(PORT, () => {
233+
console.log(`Server running on port ${PORT}`);
234+
});
207235
}
208236

209237
// Export for Testing

apps/public-api/src/app.js

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -143,16 +143,18 @@ if (process.env.NODE_ENV !== 'test') {
143143
};
144144

145145
const bootstrap = async () => {
146-
await connectDB();
147-
startWorkers();
148-
149-
const server = app.listen(PORT, () => {
150-
console.log(`Server running on port ${PORT}`);
151-
});
152-
153146
// SHUTDOWN
154-
const gracefulShutdown = async () => {
155-
console.log('🛑 SIGTERM/SIGINT received. Shutting down gracefully...');
147+
let shuttingDown = false;
148+
let server;
149+
150+
const gracefulShutdown = async (err = null) => {
151+
if (shuttingDown) return;
152+
shuttingDown = true;
153+
console.log('🛑 SIGTERM/SIGINT/Fatal Error received. Shutting down gracefully...');
154+
155+
if (err) {
156+
console.error('Fatal Error causing shutdown:', err);
157+
}
156158

157159
// Force close after 10s
158160
const forceShutdown = setTimeout(() => {
@@ -169,22 +171,49 @@ if (process.env.NODE_ENV !== 'test') {
169171
}
170172
}
171173

172-
server.close(async () => {
173-
console.log('✅ HTTP server closed.');
174+
if (server) {
175+
server.close(async () => {
176+
console.log('✅ HTTP server closed.');
177+
try {
178+
await mongoose.connection.close(false);
179+
console.log('✅ MongoDB connection closed.');
180+
clearTimeout(forceShutdown);
181+
process.exit(err ? 1 : 0);
182+
} catch (dbErr) {
183+
console.error('❌ Error closing MongoDB connection:', dbErr);
184+
process.exit(1);
185+
}
186+
});
187+
} else {
174188
try {
175189
await mongoose.connection.close(false);
176-
console.log('✅ MongoDB connection closed.');
177190
clearTimeout(forceShutdown);
178-
process.exit(0);
179-
} catch (err) {
180-
console.error('❌ Error closing MongoDB connection:', err);
191+
process.exit(err ? 1 : 0);
192+
} catch (dbErr) {
181193
process.exit(1);
182194
}
183-
});
195+
}
184196
};
185197

186-
process.on('SIGTERM', gracefulShutdown);
187-
process.on('SIGINT', gracefulShutdown);
198+
process.on('SIGTERM', () => gracefulShutdown());
199+
process.on('SIGINT', () => gracefulShutdown());
200+
201+
process.on('unhandledRejection', (reason, promise) => {
202+
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
203+
gracefulShutdown(reason);
204+
});
205+
206+
process.on('uncaughtException', (err) => {
207+
console.error('Uncaught Exception:', err);
208+
gracefulShutdown(err);
209+
});
210+
211+
await connectDB();
212+
startWorkers();
213+
214+
server = app.listen(PORT, () => {
215+
console.log(`Server running on port ${PORT}`);
216+
});
188217
};
189218

190219
bootstrap().catch((err) => {

apps/python-service/main.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,18 @@
55
async def health_check():
66
return {"status": "ok"}
77

8+
import logging
9+
from fastapi import Request
10+
from fastapi.responses import JSONResponse
11+
12+
@app.exception_handler(Exception)
13+
async def global_exception_handler(request: Request, exc: Exception):
14+
logging.error(f"Unhandled exception: {exc}", exc_info=True)
15+
return JSONResponse(
16+
status_code=500,
17+
content={"detail": "Internal server error"},
18+
)
19+
820
from routers import ai
921

1022
app.include_router(ai.router)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Component } from 'react';
2+
3+
export class ErrorBoundary extends Component {
4+
state = { hasError: false, error: null };
5+
6+
static getDerivedStateFromError(error) {
7+
return { hasError: true, error };
8+
}
9+
10+
componentDidCatch(error, info) {
11+
console.error('ErrorBoundary caught:', error, info);
12+
}
13+
14+
render() {
15+
if (this.state.hasError) {
16+
return (
17+
<div style={{ padding: '2rem', textAlign: 'center', fontFamily: 'sans-serif' }}>
18+
<h2 style={{ color: '#e53e3e' }}>Something went wrong.</h2>
19+
<p>Please refresh the page or try again later.</p>
20+
</div>
21+
);
22+
}
23+
return this.props.children;
24+
}
25+
}

apps/web-dashboard/src/main.jsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,18 @@ import { BrowserRouter } from 'react-router-dom';
55
import App from './App.jsx';
66
import { AuthProvider } from './context/AuthContext.jsx';
77
import './index.css';
8+
import { ErrorBoundary } from './components/ErrorBoundary.jsx';
89

910
const rootElement = document.getElementById('root');
1011

1112
ReactDOM.createRoot(rootElement).render(
1213
<React.StrictMode>
13-
<BrowserRouter>
14-
<AuthProvider>
15-
<App />
16-
</AuthProvider>
17-
</BrowserRouter>
14+
<ErrorBoundary>
15+
<BrowserRouter>
16+
<AuthProvider>
17+
<App />
18+
</AuthProvider>
19+
</BrowserRouter>
20+
</ErrorBoundary>
1821
</React.StrictMode>
1922
);

0 commit comments

Comments
 (0)