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.