Commit First, Code Later: How Writing the Commit Message Upfront Changes Everything
Quick context (why you're writing this)
Here's the thing: I was knee‑deep in a production bug that only showed up for users with a missing profile picture. I fired up git log --oneline and saw a string of commits like “fix stuff”, “update utils”, “wip”. After 30 minutes of scrolling I still had no clue which change introduced the regression. I ended up bisecting manually, spending two hours just to find the offending line—a missing null check that could have been spotted in seconds if the commit message had actually told me why the change existed. That moment made me rethink how I treat commits, and it’s a habit that’s stuck ever since.
The Insight
The best practice that changed how I write code is simple: write the commit message before you write any code. When you force yourself to articulate the purpose of the change up front, you naturally start thinking in smaller, intent‑driven steps. The commit becomes a contract: “I will do X to achieve Y”. If you can’t finish that sentence, you probably haven’t figured out what you’re really trying to do. This habit leads to:
- Atomic, focused changes – no giant “refactor everything” blobs that hide a dozen unrelated tweaks.
- Better code design – you end up extracting functions, renaming variables, or adding tests because the message demands a clear reason.
-
Painless history –
git bisect, code reviews, and even solo retrospectives become trivial when each commit tells a story.
It’s not about perfection; it’s about giving your future self (and your teammates) a map instead of a maze.
How (with code)
Let’s walk through a tiny feature: “Show a default avatar when a user hasn’t uploaded a profile picture.”
The mistake – committing after the fact, with a vague message
git commit -m "fix avatar display"
// userProfile.js – before any thought about intent
function getAvatarUrl(user) {
// I just slapped this together; no comment, no test
if (user.profile && user.profile.avatar) {
return user.profile.avatar;
}
// fallback hard‑coded later, but I forgot to handle missing profile entirely
return '/images/default-avatar.png';
}
What happened? I wrote the function, realized I also needed to handle the case where user.profile is null, added a quick || check, and committed. The message “fix avatar display” tells nobody why the code changed, what edge case I covered, or why I chose that particular fallback. Six months later, someone (maybe me) sees this function, wonders why the null check exists, and might accidentally remove it while “cleaning up”.
The fix – writing the commit first
Before opening the editor, I jot down:
feat: show default avatar when user.profile is missing or avatar empty
- Ensure getAvatarUrl returns a safe fallback URL for:
* users with no profile object
* users whose profile.avatar is an empty string
- This prevents broken image tags on the profile page and improves
first‑load experience for new users.
Now I know exactly what I’m aiming for. The code follows naturally:
// userProfile.js – after committing the intent first
/**
* Returns the URL to display for a user's avatar.
* Falls back to the default avatar when the profile or avatar
* field is missing or empty.
*
* @param {Object} user - The user record from the API
* @returns {string} Absolute or relative URL to an image
*/
function getAvatarUrl(user) {
const avatar = (user.profile || {}).avatar;
return avatar && avatar.length ? avatar : '/images/default-avatar.png';
}
A couple of notes that emerged because the message forced me to think:
- I added a JSDoc comment – the why is now right above the function.
- I used a safe default object
(user.profile || {})to avoid the extraif. - I kept the function pure and easy to test; later I wrote a quick unit test for the three scenarios listed in the commit message.
If I later need to revert or cherry‑pick this work, the commit message alone tells me exactly what behavior I’m adding or removing.
Why This Matters
You might think, “I can just write a good message after I’m done.” In practice, the opposite happens: the code shapes the message, not the other way around. When you write the message first:
- You catch scope creep early – if you realize you’re trying to solve two unrelated problems in one commit, you split them before writing a line of code.
- Reviews become conversations about intent, not nitpicks – reviewers can see whether the change matches the stated goal, leading to faster approvals.
- Bisecting works – a bad commit is obvious because its message describes a single, testable hypothesis.
- Even solo developers benefit – six months from now you’ll thank yourself when you’re hunting a bug in your own project and the log reads like a changelog instead of a laundry list.
It’s not a silver bullet; sometimes a big refactor legitimately needs a few related commits, and that’s fine. The rule is simply: start with the sentence that explains why you’re touching the code. If you can’t write that sentence, you probably aren’t ready to code yet.
Give it a try
Next time you pick up a task—whether it’s fixing a typo, adding a feature, or cleaning up a module—open your editor, type out the commit message first, stare at it for a few seconds, and let it guide the code you write. Notice how your thought process shifts, how the diff shrinks, and how you feel when you later glance at git log.
What’s the first commit message you’ll write before you type a single line of code? Drop it in the comments or tweet it with #CommitFirst—let’s see how this small habit changes the way we build software. 🚀
Top comments (0)