Git error messages: a field guide to the confusing parts

Git's error messages have a reputation for being cryptic. After years of daily use, it's easy to forget that many of them are genuinely ambiguous — or that they presume knowledge most users don't have. The messages that follow are some of the most common sources of confusion, along with notes on what's actually happening and how to get unstuck.

Improving these messages isn't a trivial problem. There's no clear way to know if a proposed rewrite is actually better, the work is often unfunded, and every change has to be translated into one of the 19 languages Git currently supports. That context makes the quirks below more understandable, if not less frustrating.

When git push rejects your branch

$ git push
To github.com:jvns/int-exposed
! [rejected]        main -> main (non-fast-forward)
error: failed to push some refs to 'github.com:jvns/int-exposed'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

$ git status
On branch main
Your branch and 'origin/main' have diverged,
and have 2 and 1 different commits each, respectively.

The same message appears whether your branch is simply behind the remote or has genuinely diverged from it. The only way to distinguish the two cases is to check git status or run git pull. It also isn't obvious which refs failed to push — the rejected branches are marked with ! [rejected] on the line immediately above, but that detail is easy to miss.

In practice, most people push a single branch at a time, so the question of which ref was rejected is usually moot. When this happens, a quick git status will show the state of the current branch, and that's typically enough to decide what to do next.

When git pull finds diverged history

$ git pull
hint: You have divergent branches and need to specify how to reconcile them.
hint: You can do so by running one of the following commands sometime before
hint: your next pull:
hint:
hint:   git config pull.rebase false  # merge
hint:   git config pull.rebase true   # rebase
hint:   git config pull.ff only       # fast-forward only
hint:
hint: You can replace "git config" with "git config --global" to set a default
hint: preference for all repositories. You can also pass --rebase, --no-rebase,
hint: or --ff-only on the command line to override the configured default per
hint: invocation.
fatal: Need to specify how to reconcile divergent branches.

This error presents a wall of options: configure pull.rebase false, pull.rebase true, or pull.ff only — locally or globally — or run git pull --rebase or git pull --no-rebase. For someone new to Git, parsing those choices mid-error is a lot to ask.

The practical summary is shorter: use git pull --rebase or git pull --no-rebase to resolve the immediate situation, and if you want a lasting preference, set it with git config pull.rebase false or git config pull.rebase true. The pull.ff only option is largely redundant since it matches Git's default behavior anyway, though that hasn't always been the case.

When faced with this, running git status confirms the branch state, and git log origin/main or git log shows the diverging commits. Usually git pull --rebase is the right resolution. If the goal is to discard local or remote work entirely — say, after committing to the wrong branch or amending a commit on a personal branch — git push --force or git reset --hard origin/main gets the job done.

Checking out a branch that doesn't exist

With git checkout, asking for a non-existent branch yields a confusing complaint about a path:

$ git checkout asdf
error: pathspec 'asdf' did not match any file(s) known to git

The confusion stems from git checkout accepting either a branch or a path as its first argument; Git can't tell which kind of argument you meant. A message like "No such branch, commit, or path" would be clearer, but the underlying ambiguity makes this genuinely hard to improve.

git switch, which only takes a branch (unless you pass -d), has a different but related problem. It reports invalid reference: asdf instead of invalid branch: asdf:

$ git switch asdf
fatal: invalid reference: asdf

The reason is that git switch tries to be helpful by also accepting tags. If you run git switch v0.1, you get:

$ git switch v0.1
fatal: a branch is expected, got tag 'v0.1'`

So "invalid reference" is Git's way of saying asdf isn't a branch, a tag, or any other ref. That would be more useful if the average user knew what a "reference" was — many don't. A practical workaround: mentally replace reference with branch in the error message. When the target is actually a tag, the message will make sense.

Detached HEAD and the HEAD^ trap

$ git checkout HEAD^
Note: switching to 'HEAD^'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:

  git switch -c 

Or undo this operation with:

  git switch -

Turn off this advice by setting config variable advice.detachedHead to false

HEAD is now at 182cd3f add "swap byte order" button

Checking out a commit rather than a branch puts you in detached HEAD state, and the warning is famously hard to digest. Lots of effort has gone into improving it, and there's no easy fix. The reliable strategy is to rely on a shell prompt that shows when you're detached, avoid making commits in that state, and run git checkout main (or any other branch) when you're done exploring.

Rebase in progress: reading git status

Not strictly an error, but still opaque:

$ git status
interactive rebase in progress; onto c694cf8
Last command done (1 command done):
   pick 0a9964d wip
No commands remaining.
You are currently rebasing branch 'main' on 'c694cf8'.
  (fix conflicts and then run "git rebase --continue")
  (use "git rebase --skip" to skip this patch)
  (use "git rebase --abort" to check out the original branch)

Unmerged paths:
  (use "git restore --staged ..." to unstage)
  (use "git add ..." to mark resolution)
  both modified:   index.html

no changes added to commit (use "git add" and/or "git commit -a")

Two things stand out. First, You are currently rebasing branch 'main' on 'c694cf8'. appears on the fifth line, not the first — so the branch being rebased isn't immediately visible. Second, in this case c694cf8 is actually origin/main; naming the remote branch would be far more informative. A shell prompt that shows rebase state is often more useful than this output.

Rebase conflicts after a file deletion

$ git rebase main
CONFLICT (modify/delete): index.html deleted in 0ce151e (wip) and modified in HEAD.  Version HEAD of index.html left in tree.
error: could not apply 0ce151e... wip

The confusing part here is what HEAD refers to. During a merge, HEAD is your current branch; during a rebase, it's the branch you're rebasing onto. That distinction is easy to forget mid-conflict. Listing branch names explicitly, as in this hypothetical rewrite, would remove the guesswork:

CONFLICT (modify/delete): index.html deleted on `main` and modified on `mybranch`

"Them" depends on what you ran

$ git status 
On branch master
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths: (use “git add/rm …” as appropriate to mark resolution) deleted by them: the_file

no changes added to commit (use “git add” and/or “git commit -a”)

Similar ambiguity afflicts the deleted by them: status line. The identity of "them" shifts with the operation:

  • During a merge, them is the branch you merged in.
  • During a rebase, them is the branch you were on when you started the rebase.
  • During a cherry-pick, them is the commit being picked.

If it's not obvious who "they" are, retracing your steps helps. Running git show main --stat or similar can refresh your memory of what's on your branch.

The git clean alphabet soup

$ git clean
fatal: clean.requireForce defaults to true and neither -i, -n, nor -f given; refusing to clean

Reading this error requires knowing what -i, -n, and -f stand for. Many long-time users never look them up — even -i for "interactive" can remain a mystery for a decade. The short version: -f deletes untracked files without asking, which is fast but risky; -i walks you through what will be removed, which is safer once you know what it does.