3 minutes
How I prevent myself from committing temporary code to production
I have a chronic problem of committing debug code to production. I often adjust code while testing out assumptions, reproducing issues, or trying alternative ways of implementing a feature or fixing a bug. When I’m committing thousands of lines per week, and with no-one reviewing my commits, occasionally some of this code slips through.
A couple months ago I had one particularly annoying piece of debugging code make it to production. We ended up in a loop of fetching user’s data when they visited the site. This caused a whole host of issues, including mobile app slowness, and over-fetching from an external service we rely on.
The thing is, I always know in the moment of writing this code, that I should return to it later or remove it before committing, but it’s really easy for that to slip out of my working memory. So I resolved to find a better way to keep the workflow I work best in, while not risking anything making it through. I quite like where I ended up so I figured I’d share.
Typescript
export const todoRelease = (x: never): any => {
if (typeof x === "function") {
// @ts-ignore
x();
} else {
console.error("TODO", x);
return x as any;
}
};
There’s quite a few ways to use this:
// Use as generic TODO logging
todoRelease("Add event tracking here")
// Use identity function to force conditionals
let isOnboarding = repertoire === null || todoRelease(true)
// Enclose whole blocks
todoRelease(() => {
console.log("Manually enabling onboarding")
setOnboarding(true)
})
The point, of course, is that all of this will fail to type-check with tsc
.
The way I have my project set up, I can freely develop locally with type-check failures, but the git hooks won’t let me commit that code. So I can totally let go of the need to remember these TODOs.
Rust
I implemented the same sort of thing for my Rust server. Necessarily less flexible because of the constraints of the type system, but still useful:
#[macro_export]
#[cfg(not(debug_assertions))]
macro_rules! todo_release {
() => {
#[cfg(not(debug_assertions))]
compile_error!("This code is not ready for production!");
};
($message:expr) => {
compile_error!("This code is not ready for production!")
};
}
#[macro_export]
#[cfg(debug_assertions)]
macro_rules! todo_release {
() => {};
($message:expr) => {
$message
};
}
// Compile-time-checked TODO
todo_release!("Validate user input");
// Force conditionals
let processUser = (foo && bar) || todo_release!(true);
This isn’t a perfect solution, as sometimes I do want to run the project in release mode during local development, but it mostly works. Then cargo check --release
will find all of these during my git hook (or the build will fail in CI anyway).
Results
This has been a game-changer for me, it sort of perfectly slots into how I write and commit code. It’s eliminated this class of human error. Curious what other people think about the problem in general, and my specific solutions.
If you play games, my PSN is mbuffett, always looking for fun people to play with.
If you're into chess, I've made a repertoire builder. It uses statistics from hundreds of millions of games at your level to find the gaps in your repertoire, and uses spaced repetition to quiz you on them.
If you want to support me, you can buy me a coffee.