You finished your work, committed everything, typed git push — and instead of your code landing on GitHub, the terminal throws an error: ! [rejected] main -> main (non-fast-forward). If you are staring at a git push rejected non fast forward message for the first time, it looks alarming, but here is the good news: your work is not lost, nothing is broken, and the fix is straightforward once you understand what Git is trying to tell you. This guide explains exactly what the non-fast-forward error means and walks you through the safe way to fix it without losing a single line of your code.

In This Guide

What “Non-Fast-Forward” Actually Means

Git’s full error usually looks like this:

Terminal showing git rebase workflow resolving a rejected push safely$ git push origin main
To https://github.com/yourname/project.git
 ! [rejected]        main -> main (non-fast-forward)
error: failed to push some refs to 'https://github.com/yourname/project.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. If you want to integrate the remote changes,
hint: use 'git pull' before pushing again.

Translated into plain English: the branch on the remote (GitHub, GitLab, wherever) contains commits that your local branch does not have yet. Git refuses to overwrite the remote’s history with your version, because doing so would silently throw away those extra commits. A “fast-forward” push is one where the remote simply moves its pointer forward along the same line of history — no new information would be lost. Your push is “non-fast-forward” because it would move the remote pointer somewhere the missing commits do not exist.

Think of it like two people editing a shared document. You both started from version 10. You wrote your changes offline and now hold version 11a. Meanwhile your teammate pushed version 11b from their laptop. When you try to upload your version, the server says: “Hold on — there is a version 11b up here you have never seen. Download it first, combine the two versions, then upload the combined result.” That is the entire philosophy behind the rejection. It is a safety feature, not a bug.

The single most important thing to understand: your local commits are safe. Git never deletes your committed work in this situation. The commits are sitting in your local repository, untouched. All you need to do is bring the remote changes down, combine them with yours, and push the combined history.

Why Your Push Gets Rejected: The 4 Common Causes

Understanding why the remote moved ahead of you helps you pick the right fix. Here are the four scenarios beginners run into most:

1. Someone else pushed to the same branch

The classic cause. A teammate pushed commits to main (or your shared branch) while you were working. Their commits are now on the remote, and your local copy is behind.

2. You pushed from a different computer

You committed and pushed from your laptop yesterday, then made more commits on your desktop today without pulling first. The remote has the laptop commits; the desktop does not know about them.

3. You edited files directly on GitHub

Quick typo fixes through GitHub’s web editor create commits on the remote that your local repository has never seen. This is extremely common for README or config file edits.

4. Your branch history was rewritten

If you (or someone) amended a commit, did an interactive rebase, or used git reset and then force-pushed, the remote’s history no longer matches yours. Git sees two divergent histories and rejects a normal push.

You can confirm exactly how far behind you are before doing anything:

git fetch origin
git status

If the output says something like Your branch is behind 'origin/main' by 2 commits, you know precisely what you are dealing with. For a visual picture of the two histories, this command is worth its weight in gold:

git log --oneline --graph --all -12

It shows your commits and the remote’s commits side by side, so you can see where they diverged.

The Safe Fix: Fetch, Rebase, Then Push

This is the fix to reach for in most situations, and it is the one professional developers use daily. The idea: download the remote’s new commits, replay your commits on top of them, then push the clean combined history.

Step 1: Fetch the latest remote state

git fetch origin

git fetch downloads the new commits from the remote but does not touch your working files. It is the “look before you leap” step, and it is always safe.

Step 2: Rebase your work onto the updated branch

git rebase origin/main

(Replace main with your branch name if different.) Git temporarily sets your commits aside, moves your branch pointer to the latest remote commit, then re-applies your commits one by one on top. The result is a straight, clean history that reads like everything happened in order.

Step 3: Resolve any conflicts

Sometimes you and the remote changed the same lines. Git pauses the rebase and marks the conflicted files. Open them, look for the <<<<<<< markers, keep the correct code, then tell Git to continue:

git add .
git rebase --continue

Repeat until the rebase finishes. If you get stuck and want to bail out entirely, git rebase --abort returns you to exactly where you started, with nothing lost. If you want a full walkthrough of conflict resolution, see our guide on resolving git merge conflicts step by step.

Step 4: Push again

git push origin main

Because your history now contains everything the remote has (plus your work on top), the push is a clean fast-forward and Git accepts it. That is the whole git push rejected non fast forward fix in four steps: fetch, rebase, resolve, push.

What if you have uncommitted changes?

Rebase refuses to start if your working tree is dirty. Stash your in-progress work first, rebase, then pop the stash back:

git stash push -m "wip before rebase"
git pull --rebase origin main
git stash pop

git pull --rebase is simply the fetch and rebase combined into one command. The stash commands move your uncommitted changes out of the way and restore them afterward.

The Other Safe Option: Merge Instead of Rebase

Rebasing rewrites your commit hashes, which is fine on branches only you use but can confuse teammates on shared branches. The alternative is to merge the remote changes into your branch, which preserves both histories exactly as they happened:

git pull --no-rebase origin main
git push origin main

This creates a merge commit that ties the two lines of history together. The history looks a little busier — you will see a “Merge branch ‘main’ of github.com/…” commit — but nothing is rewritten, so it is the friendlier choice when multiple people work on the same branch.

Which should you pick? As a rule of thumb: rebase on your own feature branches, merge on shared branches like main. Many teams enforce this with repository settings. When in doubt, ask your team which convention they follow.

One related skill worth learning: if you realize the problem was actually a bad commit on your side rather than remote changes, you may want to undo your last git commit while keeping your changes before sorting out the push.

When Force Push Is (and Isn’t) the Answer

You will find plenty of advice online saying “just use git push --force.” Treat that advice with suspicion. Force push tells Git: “Forget what is on the remote, use my version.” Any commits on the remote that you do not have locally are permanently deleted from the branch. On a shared branch, that can wipe out your teammates’ work.

Never force push to main, master, develop, or any branch other people use. There is no “undo” button for the remote side once the commits are gone — recovery depends on someone having a local copy.

The one situation where force push is legitimate: a private feature branch that only you use, after you intentionally rewrote its history (for example, with an interactive rebase to clean up commits before a pull request). Even then, use the safer variant:

git push --force-with-lease origin feature-branch

--force-with-lease checks that nobody else pushed to the branch since you last fetched. If they did, it refuses — turning a potentially destructive action into a safe one. Make it a habit to never type a bare --force.

What to Do If Someone Already Force-Pushed

If a teammate force-pushed and your commits vanished from the remote, do not panic and do not push yet — pushing now could make recovery harder. First, check whether your commits still exist locally:

git reflog

The reflog is Git’s diary of every move your branch pointer has made, including commits that are no longer on any branch. Find the commit hash from before the force push, then create a recovery branch from it:

git branch recovery <commit-hash>
git log --oneline recovery

If your work is there, you can cherry-pick or merge it back onto the current branch and push normally. The official Git documentation on git push and git reflog covers these commands in full detail if you want to go deeper.

The real lesson: on shared branches, a rejected push is Git protecting the team. The correct response is always to integrate (rebase or merge), never to override.

How to Prevent This Error in the Future

A few habits make the non-fast-forward rejection rare:

  • Pull before you start working. Begin each session with git pull (or git pull --rebase) so your local branch starts current.
  • Push and pull from one machine at a time, or always pull first when switching machines.
  • Avoid editing files on GitHub’s website for repositories you also work on locally — or pull immediately after.
  • Keep feature branches short-lived. The longer a branch lives, the more likely the remote moves ahead of it.
  • Set pull to rebase by default so your everyday git pull keeps history clean: git config --global pull.rebase true.

Frequently Asked Questions

Will I lose my work if my push is rejected?

No. A rejected push changes nothing — your local commits, staged files, and working directory are exactly as they were. The rejection only means Git refused to update the remote. Follow the fetch-and-rebase steps above and your work will be safe throughout.

What is the difference between git pull and git fetch?

git fetch downloads remote changes without touching your files — it is always safe. git pull is fetch plus an integration step (merge or rebase) that modifies your branch. When diagnosing a rejected push, fetch first so you can inspect the situation before changing anything.

Can I just delete my local branch and re-clone?

You could, but it is the nuclear option and unnecessary. Re-cloning loses your local branches, stashes, and configuration. The rebase or merge fix takes about a minute and preserves everything. Save re-cloning for genuinely corrupted repositories.

Why does this keep happening on the same branch?

Usually because someone (or an automated bot, like a dependency updater) pushes to that branch frequently. Pull with rebase before every push, or switch to a personal feature branch and open pull requests instead of pushing directly to the shared branch.

Is --force-with-lease really safer than --force?

Yes, meaningfully. --force overwrites the remote no matter what. --force-with-lease first verifies the remote is exactly where you last saw it; if anyone pushed in the meantime, it aborts. It turns force push from “overwrite everything” into “overwrite only if nothing changed since I looked.”

Key Takeaways

  • A “non-fast-forward” rejection means the remote has commits you do not have yet — Git is protecting shared history, not reporting an error in your work.
  • The safe fix: git fetch origin, then git rebase origin/main (or git pull --rebase origin main), resolve any conflicts, and push again.
  • On shared branches, prefer merging (git pull --no-rebase) over rebasing to avoid rewriting history others depend on.
  • Never force push to shared branches. On private branches, use --force-with-lease, never bare --force.
  • If a force push already wiped commits, git reflog can usually recover them locally before you push anything new.
  • Prevent repeats: pull before you start, avoid web-editor commits on active branches, and keep feature branches short-lived.

Conclusion

The git push rejected non fast forward error feels like a crisis the first time you see it, but it is one of Git’s friendliest guardrails: it stops you from accidentally erasing someone else’s work. Remember the four-step rhythm — fetch, rebase, resolve, push — and you will clear it in under a minute while keeping every commit intact. Make pulling a habit at the start of each work session, reserve force push for your own private branches (with --force-with-lease), and this error will go from alarming to routine. Your code was never in danger; now you know how to get it where it belongs.

Leave a Reply

Your email address will not be published. Required fields are marked *