The Silence of the Errors
Make errors impossible to ignore by making them annoying: use sounds and alerts in development, send notifications in production. Catch client and server errors early and hold yourself accountable to fix them immediately.
If you’ve built anything in TypeScript, you know how easy it is for errors to slip by unnoticed. The result? Bugs that should have been caught early end up in front of your users. This article is not about handling errors. It’s about increasing error visibility as early as possible.
Errors are easy to ignore
Let’s be honest. Do you have both your server and your browser log open when writing web software? Do you repeatedly look for error logs in there? Are your errors also buried under other logs like HMR logs or other informative logs?
If you are like me, chances are high that errors in your server or client code slip by unnoticed.
Whats easy to ignore will be ignored
I want to force myself to properly handle errors as early as possible. Ideally right at the time of writing. The solution is simple: make them really, really annoying during development. Here’s how I do it:
Annoying Server Errors with Sound Effects
For webservers we cannot rely on errors being logged to the console. When you’re not watching the error log will get buried On the server, I use a function that logs the error and makes my Mac beep:
export function annoyingErrorLogger(...message: Array<string>) { console.error(message); if (env.NODE_ENV === "development") { exec("afplay /System/Library/Sounds/Ping.aiff"); return; }}
Whenever an error occurs, I need this function to be called. Thankfully, NodeJS has us covered here with the uncaughtException and unhandledRejection events:
process.on("uncaughtException", (error) => { annoyingErrorLogger(`Uncaught Exception: ${error}`);});
process.on("unhandledRejection", (reason) => { annoyingErrorLogger(`Uncaught Rejection: ${reason}`);});
With these few lines of code, my Mac makes annoying ping sounds whenever I forget to handle any error appropriately. Make sure to not let a framework on the server swallow errors. In my case, Hono was catching errors that were thrown inside of the routes.
app.onError((err, c) => { // keep support for throwing HTTPExceptions in routes if (err instanceof HTTPException) { return c.json( { message: err.message, underlyingError: err.cause }, err.status, ); } // be annoying for everything else annoyingErrorLogger( `unexpected internal server error for request to ${c.req.path}:`, `${err}`, ); return c.json({ message: `Unexpected internal server error` }, 500);});
Annoying Client Errors with blocking Alerts
On the client you can go with a similar setup. Here the document window provides the necesary events.
window.addEventListener("error", (message, source, lineno, colno, error) => { let errorLog = `Error: ${message}\nSource: ${source}\nLine: ${lineno}\nColumn: ${colno}`; if (error && error.stack) { errorLog += `\nStack trace:\n${error.stack}`; } annoyingErrorLogger(errorLog);});
window.addEventListener("unhandledrejection", (event) => { let reason = event.reason; let rejectionLog = `Unhandled promise rejection: ${reason}`; if (reason instanceof Error && reason.stack) { rejectionLog += `\nStack trace:\n${reason.stack}`; } annoyingErrorLogger(rejectionLog);});
The annoyingErrorLogger
can issue an alert()
that needs to be confirmed.
This holds me accountable.
When I’m coding and something breaks, I can’t just ignore it and move on.
Annoying Errors in Production: Telegram Bots
In production, you want errors to be just as hard to ignore. My favorite trick: send them to a Telegram chat using a bot. A bot is straightforward to create and sending a message is as simple as a fetch request.
export function annoyingErrorLogger(...message: Array<string>) { console.error(message); if (config.NODE_ENV === "development") { exec("afplay /System/Library/Sounds/Ping.aiff"); return; } let url = `https://api.telegram.org/${config.TELEGRAM_BOT_TOKEN}/sendMessage`; let payload = { chat_id: config.TELEGRAM_CHAT_ID, text: [`[${config.NODE_ENV}]`, ...message].join("\n"), }; fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }) .then(async (res) => ({ ok: res.ok, txt: await res.text(), status: res.status, })) .then(({ ok, txt, status }) => { if (ok) { console.info( `Telegram message sent successfully. Status: ${status}, Response: ${txt}`, ); } else { console.error(`Telegram message failed with status ${status}: ${txt}`); } }) .catch((err) => console.error("Error sending Telegram message:", err));}
Now, every time something goes wrong in production, I get a message in my Telegram error channel. No more silent failures.
Annoying Client Errors in Production
To catch client-side errors in production, set up an endpoint on your server that triggers the annoying error logger:
let errorLoggerApp = new Hono<AuthenticatedContext>() .use(authMiddleware) .use(onlyAuthenticated) .post("/log", describeRoute({ hide: true }), async (c) => { annoyingErrorLogger( "an error was sent from the client:", await c.req.text(), ); return c.json({ message: "ok" }); });
app.route("/api/error", errorLoggerApp);
This is free. It makes client and server errors beep during development and gives you a Telegram error chat in production.
Negative space programming: make the impossible, impossible to ignore
This approach works beautifully with the concept of “negative space programming”. For every state that should never happen, make it annoying and obvious:
export function shouldNeverHappen(msg: string, ...args: any[]): never { console.error(msg, ...args); if (isDevEnv()) { debugger; } throw new Error(`This should never happen: ${msg}`);}
If you ever hit this code, you’ll know your assumptions are wrong, and you’ll be forced to fix them.
Conclusion
Errors are easy to ignore. Make them annoying so they are hard to ignore. Hold yourself accountable. You’ll catch more bugs, fix them faster.
Try it. Your future self will thank you.