Git Good
Git Good
Git Good
The Worst Error Messages: A Horror Story in Plain Text
S2 E2421m · Apr 05, 2026
A single word in red—"fatal"—has been viewed 15 million times on Stack Overflow, and developers still don't know what Git just killed.

The Worst Error Messages: A Horror Story in Plain Text

Fatal

Somewhere right now, at this very moment, a developer is staring at their terminal. They have just done something with Git, and Git has responded with a single word in red. Fatal. Not "there is a problem." Not "here is what went wrong." Fatal. As if the repository has died. As if there is no coming back. The developer does not know what they did wrong. They do not know what "fatal" means in this context. They do what every developer in this situation does. They copy the error message, paste it into a search engine, and hope that someone on Stack Overflow had the same panic before them.

The question "How do I undo the most recent local commits in Git?" has been viewed over fifteen million times. Fifteen million moments of quiet dread from people who use this tool every single day and still do not understand what it does when something goes sideways. Four of the ten most viewed questions on all of Stack Overflow, across every programming language and framework and database and cloud service in existence, are about Git. Not about algorithms. Not about memory management. About a version control tool that has been around for over twenty years.

The developers suffering most are the ones who learned Git by copying commands from tutorials, never building a mental model of what those commands do. Poor Git education does not just produce bad habits. It produces people who are completely unprepared for the moment Git stops working.

This is not a story about a bad tool. Git's internals are elegant. Its data model is one of the most beautifully simple designs in all of computing, four object types, a content-addressed store, a directed acyclic graph. If you have listened to episode five of this series, you know how clever it is under the hood. This is a story about the gap between that elegance and what Git actually says to the people trying to use it. It is a story about the worst error messages in computing, the people who wrote them, and the people trying to fix them.

Built for Kernel Hackers

To understand why Git talks the way it does, you have to understand who it was talking to in the beginning. When Linus Torvalds built Git in April two thousand five, he was not building a tool for the general developer population. He was building a tool for Linux kernel hackers, the few hundred people on Earth who could keep a three hundred megabyte codebase in their heads and argue about memory allocation strategies over email.

The original interface was what Git still calls "plumbing." Low-level commands that manipulated the object database directly. Commands like hash-object, cat-file, update-index, write-tree, commit-tree. These are not names designed for humans. They are names designed for shell scripts. Linus assumed the user already understood what a tree object was, what a blob was, what it meant to update an index. He assumed this because every user he cared about did understand these things.

I am an egotistical bastard, and I name all my projects after myself. First Linux, now Git.

The "porcelain" layer, the human-facing commands like commit, branch, merge, and the infamous checkout, came later. Other contributors built them on top of Linus's plumbing. But they inherited the plumbing's assumptions. The error messages assumed you knew what a reference was. They assumed you knew what HEAD meant. They assumed you understood the difference between the working tree, the index, and the repository. If you did not understand these things, the error messages were not going to teach you. They were going to use words you had never seen and then tell you the operation was fatal.

This was not malice. It was audience design. Linus built Git for Linus and people like Linus. The problem is that the audience changed. The tool stayed the same.

The Vocabulary Problem

Here is what happens when you try to check out a branch that does not exist. You type git checkout asdf, and Git responds with: "error: pathspec 'asdf' did not match any file known to git." You wanted to switch branches. Git is talking about pathspecs and files. The word "branch" does not appear anywhere in the message. A developer who does not know what a pathspec is, which is most developers, has no idea what just happened. They do not know if they made a typo, if the branch was deleted, if they are in the wrong repository, or if something is fundamentally broken.

The newer command, git switch, does slightly better. Type git switch asdf and you get "fatal: invalid reference: asdf." At least the word "fatal" tells you it failed. But now the word is "reference." A lot of Git users have literally no idea what a reference is. It is not a word from their vocabulary. They think in terms of branches and tags and commits. "Reference" is the internal abstraction that unifies all three, and it is a perfectly reasonable term if you are a Git contributor who thinks about the object model all day, but it is gibberish if you are a frontend developer who just wants to switch to the feature branch.

This is the pattern that runs through almost every confusing Git message. Git talks in Git's vocabulary, not the user's vocabulary. The staging area is called the "index." The current position in history is called "HEAD." A file path is called a "pathspec." Your local copy of a remote branch is called a "remote-tracking reference." Each of these terms is technically precise. Each of them is meaningless to someone who has not read the Git glossary, and almost nobody has read the Git glossary.

Julia Evans, who writes some of the best technical explanations on the internet, cataloged this problem in detail in two thousand twenty-four. She walked through error message after error message, showing how each one uses internal vocabulary where human vocabulary would be clearer. Her suggested fix for most of them is almost embarrassingly simple. Just say "branch" instead of "reference." Say "file" instead of "pathspec." Say "your current branch" instead of "HEAD."

The problem is not that Git cannot communicate clearly. The problem is that Git communicates in a language its users do not speak.

You Are in Detached HEAD State

Of all the messages Git has ever produced, none has terrified more beginners than this one. You are in detached HEAD state. For someone who does not know what HEAD means, this sounds like something has gone catastrophically wrong. Something has been detached. The word HEAD suggests the most important part of something. And the word "state" implies you are stuck in a condition you did not ask for.

What actually happened is mundane. You checked out a specific commit instead of a branch. Your working directory now points directly to that commit rather than following a branch name. This is a perfectly normal and useful thing to do. You might want to look at what the code looked like three weeks ago. You might want to test an old version. Detached HEAD is fine. It is safe. Nothing is broken.

But Git does not say "you are looking at a specific commit." It says you are in detached HEAD state, and then it prints a paragraph of advice about creating new branches and using git switch with a detach flag, which introduces yet another term the user has never encountered. The advice is technically correct and practically useless for the person reading it, who mostly just wants to know if they have broken something and how to get back.

This is what Git's advice system does. It was added over the years as a way to give users more context after potentially confusing operations. When you push and it gets rejected, Git prints hints. When you end up in detached HEAD, Git prints hints. When you are in the middle of a rebase and run git status, Git prints hints. The intention is good. The execution assumes a level of understanding that the person reading the hints almost certainly does not have. If they understood the hints, they would not need the hints.

The deeper irony is that Git knows it has a communication problem. The advice system is proof. Someone, at some point, looked at "detached HEAD state" and said, this needs more explanation. And then they wrote the explanation in the same vocabulary that caused the confusion in the first place.

One Command to Rule Them All

If you want to understand the single biggest UX failure in Git's history, look at checkout. The command git checkout does three completely different things depending on how you call it. Git checkout main switches to the main branch. Git checkout with two hyphens and a filename restores a file to its last committed state. Git checkout with a commit hash puts you in detached HEAD state. Three operations. Three mental models. One command.

This was not a deliberate design decision. It was an accident of history. In the early days, checkout was the command that updated your working directory. Switching branches updates your working directory. Restoring a file updates your working directory. Looking at an old commit updates your working directory. So they all ended up in the same command. It made sense from the implementation's perspective. It made no sense from the user's perspective.

The consequences are real. A developer who means to switch branches but forgets the two hyphens can accidentally overwrite a file they have been working on. Git will not warn them. Git will not ask for confirmation. The file is just gone, silently replaced with the version from the last commit. There is no undo. There is no recycle bin. If the changes were not staged or committed, they are lost.

In November two thousand eighteen, a Vietnamese developer named Nguyễn Thai Ngoc Duy, who goes by the handle pclouds on the Git mailing list, posted a proposal. He wanted to split checkout into two new commands. Git switch would handle branch operations. Git restore would handle file operations. Each command would do one thing, and each would have sensible defaults and clear error messages.

The purpose of this command is to avoid the confusion of one-command-do-all git checkout for new users.

The patches landed in Git two point twenty-three in August two thousand nineteen. Both commands were marked "experimental." And here, five years later, they are still marked experimental. Git checkout has not been deprecated. It will never be deprecated. There are millions of scripts, tutorials, muscle memories, and CI pipelines that use checkout. You cannot take it away without breaking the world.

This is the backwards-compatibility tax. Git can add better commands. It cannot remove the bad ones. Every improvement is additive. The confusion does not decrease. It just gets a new layer on top.

The Ones Trying to Fix It

The effort to make Git less hostile is not one person's crusade. It is a slow, distributed, slightly uncoordinated push by people who care enough to do the unglamorous work of rewriting error messages and arguing about wording on a mailing list.

Jeff King, who goes by Peff, spent eleven years at GitHub where his job was, in his words, mainly about improving Git. He is one of the most prolific contributors in Git's history, with thousands of commits to the project. In one mailing list discussion about error message standards, he acknowledged that Git has not been very consistent about how it formats and presents errors. He had laid out his mental model for how error messages should work years earlier, but getting consensus in a project with dozens of active contributors and decades of accumulated conventions is slow work.

We have not been very consistent about this. I think laying out guidelines could help, but the real work is in reviewing patches carefully and pushing back when messages are unclear.

Emily Shaffer, a software developer at Google, has worked on Git from a different angle. Her most important contribution might be the "My First Contribution" tutorial, a guide for people who want to start contributing to Git itself. She wrote it with a specific philosophy.

Writing a new contributor document works well if you already do not know what is going into the document. You catch the assumptions because you are bumping into them yourself.

That sentence contains the diagnosis for Git's entire communication problem. The people who write the error messages already know what the error messages mean. They do not bump into the assumptions because they do not share them. Emily also proposed a Git-project-blessed blog for making Git better understood in the rest of the world, an acknowledgment that the tool needed to explain itself outside of man pages and mailing list archives.

And then there is Scott Chacon. He co-founded GitHub. He wrote Pro Git, the free book that functions as the closest thing Git has to an official textbook, translated into over fifteen languages. He has given over a hundred talks explaining Git to confused audiences around the world. And now he is building GitButler, a desktop application that wraps Git in a graphical interface designed to expose what Git is actually doing without requiring you to decode its vocabulary.

Most developers actively avoid the advanced features. Not because they are unnecessary, but because they are hard to visualize, easy to misuse, and painful to recover from when something goes wrong. Most teams end up with a simplified workflow not because it is optimal, but because it is survivable.

That word. Survivable. It says everything about the gap between Git's power and Git's communication. Teams do not choose simple workflows because simple is better. They choose simple because Git's interface makes advanced operations feel dangerous even when they are not.

The "Us" and "Them" Problem

There is a specific class of Git message that deserves its own special mention, and it involves pronouns. When you are in the middle of a merge and run git status, Git might tell you that a file was "deleted by them." When you are rebasing, the same situation might say "deleted by us." The problem is that "us" and "them" switch meaning depending on which operation you are performing.

In a merge, "ours" means your branch and "theirs" means the branch you are merging in. In a rebase, "ours" means the branch you are rebasing onto and "theirs" means your commits being replayed. So during a merge, your changes are "ours." During a rebase, your changes are "theirs." The same developer, working on the same code, finds their work described by opposite pronouns depending on which command they used.

This is not a quirk. This is an active source of data loss. A developer resolving a conflict during a rebase who picks "ours" thinking it means their own work will accidentally keep the other branch's version and throw away their changes. The terminology is not just confusing. It is dangerous.

Julia Evans flagged another related problem. During a rebase conflict, Git shows a message like: "CONFLICT: index dot html deleted in zero c e one five one e and modified in HEAD." The commit hash is not human-readable. And HEAD, in a rebase, does not mean what you might think. It does not refer to where you were before the rebase started. It refers to the commit that is currently being applied, which shifts with every step of the rebase. The same word means different things at different moments within a single operation.

Evans suggested a fix that seems almost too obvious. Instead of saying "deleted in zero c e one five one e and modified in HEAD," just say "deleted on main and modified on mybranch." Use branch names. Use words that map to what the developer actually cares about. The information is available. Git knows which branches are involved. It just chooses to show commit hashes and internal vocabulary instead.

The Deeper Question

Here is where this story gets philosophical, and where reasonable people genuinely disagree. Is Git's learning curve a design flaw, or is it an honest reflection of genuine complexity?

The case for design flaw is strong. The same operations could be communicated with clearer vocabulary, more consistent pronouns, and error messages that tell you what went wrong in words you understand. The checkout mess was not inevitable. The detached HEAD message could say "you are looking at a specific commit, not a branch." The push rejection could tell you whether your branch is behind or has diverged, because those require different fixes and Git knows which case it is. These are not deep technical problems. They are word choices.

But the case for inherent complexity is real too. Git is a distributed version control system with content addressing and history rewriting. A branch really is a pointer to a commit, not a folder full of files. A merge really does create a new commit with two parents, and the conflicts really do require human judgment to resolve. Rebasing really does replay commits onto a new base, and the intermediate states really are confusing because you are in the middle of an operation that has not finished yet. Any simplified explanation will eventually fail the person who believed it.

GitButler exists to close that gap. It does not replace Git. It exposes Git.

The Git contributors who resist simplifying the messages are not being stubborn. Some of them genuinely believe that using accurate technical terms, even if those terms require learning, is more respectful to the user than using simplified terms that will mislead them later. If you teach someone that a branch is a folder, they will be confused when two branches share the same files. If you teach them that HEAD means "your current branch," they will be confused when HEAD is detached. Every simplification is a future misunderstanding waiting to happen.

The counter-argument is that you can be accurate without being obscure. You can say "branch" instead of "reference" without lying about what a reference is. You can say "your changes" instead of "theirs" during a rebase without misrepresenting what is happening. You can show branch names instead of commit hashes without hiding the commit hashes for those who want them. Clarity and accuracy are not opposites. They just require more effort than dumping internal state to the terminal.

A Git That Explains Itself

The trajectory is clear, even if the pace is slow. Git is getting better at talking to humans. The advice system, for all its flaws, shows that the project acknowledges the problem. The switch and restore commands show a willingness to break with tradition when the UX cost is too high. Translation efforts mean that error messages are at least being thought of as human-readable text rather than debug output. And individual contributors keep filing patches that change one message at a time, replacing "reference" with "branch," adding context where there was none, turning "fatal" into something that actually helps.

The external ecosystem matters too. Julia Evans drawing pictures of what Git is doing. Katie Sylor-Miller meeting people in their moment of panic. Scott Chacon writing a free book and then building a graphical client when the book was not enough. These people are all doing the work that Git's interface was supposed to do, translating between the elegant machine and the confused human standing in front of it.

Now that translation is happening in real time, inside the terminal, by AI. GitHub Copilot suggests the command you probably meant. Language model chat interfaces explain what "detached HEAD state" means in plain English and walk you through getting out of it. Some editors now intercept Git error messages and annotate them with human-readable context before the developer even sees them. The message still says "fatal: pathspec did not match any file known to git," but a sidebar quietly says "it looks like you mistyped a branch name."

This is genuinely useful. For someone who would have spent twenty minutes on Stack Overflow, an immediate plain-English explanation is a real improvement. But there is a version of this that goes badly. If AI always catches the confusion before the developer has to feel it, that developer never develops the fluency to work without the AI. The error messages stop being a wall that forces you to learn something. They become noise the tool handles for you. And when the AI is wrong, or unavailable, or confidently suggests the wrong command, you have no model to fall back on. Git's errors were always asking you to understand something. The question is whether handing that translation to AI is fixing the problem or bypassing it.

These error messages are also the first wall that teams hit when they migrate from other version control systems. Moving from SVN or Perforce to Git, you bring muscle memory that does not transfer, and the messages you encounter in your first week tell you nothing useful about why your mental model is wrong.

Twenty years after Linus wrote the first version in two weeks, Git is still the most powerful version control system ever built. It is also still the one that tells you something is "fatal" when all you did was mistype a branch name. The internals have not changed. They are still elegant. The question has always been whether the tool can learn to explain its own elegance, or whether it will always need translators standing between it and the people trying to use it.

The answer, so far, is both. Git is slowly learning to speak human. And humans keep building bridges for the ones who cannot wait.

To check if your version of Git has the newer commands, type git switch with two hyphens and help. If it works, you have them. Try switching branches with git switch instead of git checkout, and restoring files with git restore instead of checkout with two hyphens. These commands each do one thing, with clearer error messages when something goes wrong. Your muscle memory will resist. Give it a week.