The Setup
I've been building a QuickBooks Online MCP server — MCP (Model Context Protocol) being the protocol that lets Claude talk to outside tools like accounting systems.
At some point during a cleanup pass, I noticed the process error handlers — process.on('uncaughtException') and process.on('unhandledRejection') — were registered inside the async main() startup function.
Looked fine at first glance.
It wasn't.
Why That's a Problem
Here's the thing about Node module initialization: it happens before your function body runs.
Static import statements at the top of your entry file execute first — hoisted, fully resolved — before a single line of your main() function fires. Which means if anything throws during module init (a missing env var, a busted connection, a misconfigured client), it throws before your error handlers exist.
You have uncaughtException and unhandledRejection handlers. They just aren't there yet.
The process crashes. Or worse — it silently swallows something and you have no idea why.
The Fix
One commit: move the error handlers to module level, before any other code.
process.on('uncaughtException', (err) => {
console.error('[uncaughtException]', err);
process.exit(1);
});
process.on('unhandledRejection', (reason) => {
console.error('[unhandledRejection]', reason);
process.exit(1);
});
// NOW do your imports, setup, main()
That's it. Two process.on calls, moved to the top. Now they're active before any module-init code can throw.
The Same Pattern Bit Me Elsewhere
I hit the exact same ordering problem in a cancer doctor finder project while writing a scripts/import-takeshape-articles.ts migration.
The script loaded .env.local inline, then statically imported src/db/index.ts. The db module read process.env.DATABASE_URL at the top level — before the env loader ran. postgres-js silently defaulted to localhost:5432, which wasn't running. The result was 143 failed imports and a wrapped error that said almost nothing useful.
What finally broke it open: logging err.cause in the catch block.
Drizzle's default error wraps the underlying query error but buries the original. You'll see "Failed query" for days until you explicitly console.error(err.cause) — and suddenly there it is: ECONNREFUSED ::1:5432. The real thing, hiding one layer down.
Two different repos, two different bugs, same root cause: I assumed my setup code ran earlier than it actually did.
Why This Matters to Me
Startup failures are the cruelest kind. The process is dead before you have any visibility. You're debugging blindfolded.
The lesson isn't "add error handlers" — most codebases have them. It's register them first, above everything. Above your imports. Above your env loading. Above your client setup. The moment Node starts executing your entry file, those handlers should be live.
And if an error still makes it through looking vague? Check .cause. The real error is usually in there, just waiting for you to look one level deeper.
P.S. The QuickBooks Online MCP server is open source — if you're building AI agents that need to talk to accounting data, it's a solid foundation. Real OAuth, real token persistence, proper MCP structure. Worth a look.