When a Node.js app works locally but crashes or returns 500 errors after deployment, the root cause is almost always an environment difference: missing environment variables, different file paths, incorrect build steps, or uncaught exceptions that only surface in production. This guide walks you through a systematic, step-by-step process to identify and fix the most common culprits, from checking logs to verifying dependencies and environment configuration.
What You’ll Need
- SSH or terminal access to your production server (or a deployment dashboard like Heroku, Render, or Vercel).
- The ability to view application logs (e.g.,
journalctl,pm2 logs, or your hosting provider’s log viewer). - Your project’s codebase locally, ideally the exact commit that was deployed.
- Access to your environment variable configuration (where you set secrets like database URLs, API keys, etc.).
Step-by-Step
- Check the logs first. The quickest way to diagnose is to look at the actual error. If you’re using PM2, run
pm2 logs. On systemd, usejournalctl -u your-app. On most PaaS platforms, there’s a “Logsâ€� tab in the dashboard. Look for the stack trace or the specific error message that appears right before the crash or 500 response. This single step often reveals the issue directly. - Verify environment variables. A missing
DATABASE_URL,JWT_SECRET, orNODE_ENVis a classic cause. Compare the variables set in your production environment against your local.envfile. Ensure all required variables are present and correctly spelled. Also check thatNODE_ENVis set toproduction(unless your app needs otherwise), as some libraries behave differently in development. - Confirm your build step ran correctly. If you’re using a frontend framework (React, Vue, etc.) or a transpiler (TypeScript, Babel), the build process must complete successfully. Check that the build output (e.g.,
dist/,build/) exists on the server and that your main entry point (e.g.,server.js) points to the correct file. Runnpm run buildlocally to see if it fails there too—if so, fix the build errors before redeploying. - Check for missing or mismatched dependencies. Run
npm ci(oryarn install --frozen-lockfile) in the production environment to install dependencies exactly as specified in your lockfile. If you’re using a platform that installs dependencies automatically, confirm that the install step didn’t skip or fail. Also verify that your Node.js version matches the one specified inenginesin yourpackage.json; using a different major version can cause native module failures. - Look for hardcoded paths or case-sensitivity issues. If your code references absolute paths like
/home/user/app/that only exist on your local machine, the app will crash on the server. Use environment variables orpath.join(__dirname, ...)instead. Also, Linux servers are case-sensitive—if you import./utils/helperbut the file is namedHelper.js, it will fail in production but work on Windows/macOS. - Test the app locally with production settings. Set
NODE_ENV=productionlocally and run your app. This often surfaces issues like conditional logging, different error handling, or minified code that breaks. If it crashes locally, you can debug more easily. - Check database connectivity and migrations. A 500 error often means the app can’t reach its database. Verify that the database host, port, username, and password are correct and that the database is accessible from the server (check firewall rules). Run any pending migrations with
npm run migrateornpx sequelize db:migrate(or your ORM’s equivalent). - Examine the process manager or container logs. If you’re using Docker, check the container logs with
docker logs <container_id>. If you’re using PM2, checkpm2 statusto see if the process is in a “erroredâ€� or “stoppedâ€� state. Look for OOM (out-of-memory) errors—if the process is killed by the OS, you’ll seeKilledin the logs. Increase memory limits if needed. - Add global error handling if it’s missing. If your app doesn’t have a catch-all error handler (e.g.,
process.on('uncaughtException')or Express error middleware), an unhandled promise rejection can crash the process. Add a simple error handler that logs the error and returns a 500 response instead of crashing. This won’t fix the root cause, but it will keep the app alive and give you a clearer log. - Redeploy after each fix. Make one change at a time, redeploy, and check the logs. This isolates the fix that works. If you’re using Git, tag the commit you’re deploying so you can roll back easily if needed.
Common Mistakes
- Ignoring the logs and guessing at the cause instead of reading the actual error.
- Forgetting to set
NODE_ENV=production, which can cause verbose logging, slower performance, or different behavior in libraries like Express. - Using
npm installinstead ofnpm ci, which may install a different version of a dependency than what you tested with. - Hardcoding secrets in the code or committing
.envfiles to the repository. - Assuming the server’s file system is identical to your local machine—paths, case sensitivity, and permissions all differ.
- Not testing the build locally before deploying, especially if you’re using TypeScript or a bundler.
Tips
- Use a process manager like PM2 or systemd to keep your app running and automatically restart it if it crashes.
- Set up a health check endpoint (e.g.,
/health) that returns a 200 status if the app is alive and can connect to the database. - Use a logging service (like Winston or Morgan) to write logs to a file or external service, not just the console, so you can review them after a crash.
- Keep your dependencies up to date, but always test after upgrading major versions.
- Use a lockfile (
package-lock.jsonoryarn.lock) and commit it to ensure consistent installs.
Troubleshooting
If the above steps don’t resolve the issue, try the following:
- Reproduce the error locally by setting the same environment variables and running the app with
NODE_ENV=production. Use a debugger likenode --inspectto step through the code. - Check the server’s system logs (e.g.,
/var/log/syslog) for OOM killer messages or other system-level errors. - Test the app with a minimal reproduction: create a new Node.js app that only does a simple HTTP request to the database. If that works, the issue is in your code, not the environment.
- Roll back to the last known working version to confirm the issue was introduced by a recent change. Then diff the changes to identify the culprit.
- Contact your hosting provider’s support if you suspect infrastructure issues (e.g., network restrictions, resource limits).
Frequently Asked Questions
Why does my Node.js app work locally but crash on the server?
Most often it’s because of differences in environment variables, file paths, or operating system behavior (like case sensitivity). The server may also have a different Node.js version or missing native modules.
How do I see the error logs for my Node.js app?
If you’re using PM2, run pm2 logs. For systemd, use journalctl -u your-service. On cloud platforms, check the dashboard’s log viewer. Always ensure your app writes logs to stdout or a file.
What does a 500 error mean in Node.js?
It means the server encountered an unexpected condition that prevented it from fulfilling the request. It’s a generic error, so you need to check the logs to see the actual exception or error message.
Should I use npm install or npm ci in production?
Always use npm ci in production. It installs dependencies exactly from the lockfile, ensuring the same versions as your local development, and it’s faster.
How do I prevent my Node.js app from crashing on unhandled rejections?
Add a global handler like process.on('unhandledRejection', (err) => { console.error(err); process.exit(1); }) or better, use a process manager that restarts the app automatically. However, the best fix is to handle the specific rejection in your code.
Why is my app returning 500 after a database migration?
This usually means the migration changed the database schema, but your code still expects the old structure. Check that your models/ORM definitions match the new schema, and verify that the migration ran successfully on the production database.
Related
General steps for current mainstream versions; menus can differ by device and version. See the disclaimer or report an error.
More in this bed
- iPhone/iPad Recovery Mode When Device Is Unavailable
- Android Button Not Working? Quick Fixes
- Prevent Malicious Package Installation on Linux
- Fix BattlEye Service Driver Load Error
- Facebook Marketplace: How to Change Currency
- Create App Folders in Windows 11 Start Menu