git switch and git checkout

git switch and git checkout are the two commands you use to move between branches in Git. git checkout is the original, do-everything command — it switches branches, restores files, and detaches HEAD onto arbitrary commits, all from one entry point. git switch is a newer, narrower command introduced specifically to make branch switching unambiguous and safer, and it is the one you should reach for by default. git checkout is still essential to know because it is everywhere in existing scripts, tutorials, and muscle memory.

Overview & How It Works

To understand branch switching you first need a clear picture of Git’s object model. Every commit Git creates is an object containing a pointer to a tree object (a snapshot of the directory structure at that point), pointers to zero or more parent commits, and metadata (author, committer, timestamp, message). A tree object in turn points to blob objects, which store the raw contents of files, and to other tree objects for subdirectories. Git does not copy everything per commit — it only stores new blobs and trees for what actually changed; anything unchanged is shared by reference, identified by its content hash.

A branch, such as main or feature/login-page, is nothing more than a small file under .git/refs/heads/ that contains a single commit hash — a lightweight, movable pointer to the tip commit of that line of work. HEAD is, in the normal case, a symbolic reference that points at one of these branch refs (you will see this written as HEAD -> refs/heads/main). When you commit, Git creates a new commit object and then moves the branch ref that HEAD points to forward, to that new commit. Switching branches does not change any history — instead, Git changes what HEAD points at and rewrites your working directory and staging area (the index) to match the snapshot recorded in the target branch’s tip commit.

That is the essence of what both git switch <branch> and git checkout <branch> do: read the target commit’s tree, compare it file by file against your working tree and index, update every file that differs, then repoint HEAD. If you have uncommitted changes that would be silently overwritten by this process, Git refuses and asks you to commit, stash, or discard them first.

It is also possible to point HEAD directly at a commit rather than at a branch. This is called detached HEAD state. In this state HEAD holds a raw commit SHA instead of a symbolic reference to a branch, so any new commits you make there are not tracked by a branch — they become unreachable and eligible for garbage collection the moment you switch away, unless you create a branch to hold onto them first.

Historically, git checkout handled all of this, plus restoring individual files from the index or another commit — a genuinely different operation that works on paths, not refs. That overload made checkout confusing: git checkout main could switch branches, but if a file happened to be named main too, Git had to guess your intent from context. Git 2.23 (released in 2019) introduced git switch and git restore to split checkout‘s responsibilities into two focused, less error-prone commands. git switch only ever operates on branches; git restore only ever operates on files. checkout remains fully supported, so you will keep encountering it, but this lesson teaches switch as the default for branch changes, with checkout explained alongside it.

Syntax

git switch

git switch [<branch>]
git switch -c <new-branch> [<start-point>]
git switch -d <commit>
git switch -
Option Meaning
<branch> Switch to an existing local branch, updating the working tree, index, and HEAD.
-c, --create <new-branch> Create <new-branch> and switch to it, starting from the current commit (or from <start-point> if given, e.g. another branch, tag, or commit).
-C, --force-create <new-branch> Like -c, but resets the branch if it already exists, discarding its previous tip at that name.
-d, --detach <commit> Check out a commit or tag directly, entering detached HEAD state instead of moving to a branch.
- Switch back to whichever branch you were on before the current one.
-f, --force Discard local changes in files that differ, instead of aborting the switch.
-t, --track When creating a branch from a remote-tracking branch, set it up to track that remote branch.

git checkout

git checkout <branch>
git checkout -b <new-branch>
git checkout <commit>
git checkout -- <path>
Option Meaning
<branch> Switch to an existing branch (same effect as git switch <branch>).
-b <new-branch> Create <new-branch> and switch to it (equivalent to git switch -c).
-B <branch> Create or reset <branch> to the current commit and switch to it.
<commit> Check out a specific commit or tag directly, entering detached HEAD state.
-- <path> Restore <path> in the working tree from the index (or from a given commit), leaving HEAD and the current branch untouched.
-f, --force Discard local changes instead of aborting.
-m, --merge Attempt a three-way merge of local changes into the new branch instead of aborting.

Examples

Example 1: Creating and switching to a new branch

Most of the time you want to start new work on its own branch. -c creates the branch and moves you onto it in a single step.

git branch
git switch -c feature/login-page

Output:

* main
Switched to a new branch 'feature/login-page'

git branch with no arguments lists local branches, marking the current one with *. git switch -c then creates feature/login-page pointing at the same commit as main and immediately repoints HEAD at it — the working tree does not change yet because both branches share the same tip commit.

Example 2: Git blocks a switch that would overwrite changes

Git will not silently discard uncommitted work when you switch branches.

echo '// work in progress' >> app.js
git switch main

Output:

error: Your local changes to the following files would be overwritten by checkout:
	app.js
Please commit your changes or stash them before you switch branches.
Aborting

Because app.js differs between the current branch and main, applying main‘s version would destroy the uncommitted edit, so Git aborts. The safe fix is to stash the change, switch, and (later) restore it:

git stash
git switch main

Output:

Saved working directory and index state WIP on feature/login-page: 3a5c9f1 fix: add login form validation
Switched to branch 'main'

Example 3: Detached HEAD by checking out a specific commit

Sometimes you want to inspect (or branch from) an old commit rather than a branch tip.

git log --oneline -3
git checkout 3a5c9f1

Output:

3a5c9f1 fix: add login form validation
9f8e7d6 docs: fix typo in README
4c3b2a1 chore: initial commit
Note: switching to '3a5c9f1'.

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.

HEAD is now at 3a5c9f1 fix: add login form validation

HEAD now points straight at commit 3a5c9f1 instead of at a branch ref. If you decide this old point in history is a good place to start real work — say, a hotfix — attach a branch to it before you do anything else:

git switch -c hotfix/login-typo

Output:

Switched to a new branch 'hotfix/login-typo'

hotfix/login-typo now points at 3a5c9f1, and HEAD is symbolic again, so any commits you make from here are safely reachable through the new branch.

Example 4: Restoring a single file

checkout‘s other historical job — discarding an unstaged edit to one file — is written with a -- before the path so Git treats what follows as a path, not a ref.

git status --short
git checkout -- app.js

Output:

 M app.js

The first line of output is from git status --short, showing app.js modified but not staged. git checkout -- app.js replaces the working-tree copy of app.js with the version from the index, printing nothing on success; a follow-up git status --short would show a clean tree. The modern, unambiguous equivalent is:

git restore app.js

Prefer git restore for this task — it can never be mistaken for a branch switch.

How It Works Step by Step

When you run git switch <branch> (or git checkout <branch>), Git performs roughly these steps internally:

  1. Resolve <branch> to a commit SHA by reading .git/refs/heads/<branch>.
  2. Read that commit object to find the tree object representing its full directory snapshot.
  3. Compare that tree, file by file, against the current index and working tree. If any tracked file has uncommitted changes that the switch would overwrite, abort with an error (unless --force or --merge is given).
  4. Write the differing files into the working directory so it matches the target tree exactly.
  5. Update the index so it also matches the target tree — staged changes now reflect the new branch’s state.
  6. Move HEAD: for a branch, rewrite HEAD to the symbolic reference refs/heads/<branch>; for a bare commit or tag, write the raw SHA into HEAD directly, producing detached HEAD state.

Nothing about the commits themselves, their parents, or their trees is touched by any of this — switching branches only ever moves pointers and updates the two things that represent your current working state, the index and the working directory.

Common Mistakes

Mistake 1: Relying on checkout when a branch and a file share a name

git checkout main

If your repository has both a branch called main and a tracked file called main, git checkout main is ambiguous — older Git versions had to guess which one you meant, and could restore the file instead of switching branches. Fix: use git switch main when you mean the branch, and git restore main (or git checkout -- main) when you mean the file. Because each command only accepts one kind of target, the ambiguity disappears.

Mistake 2: Losing commits made in detached HEAD

git checkout 3a5c9f1
git commit --allow-empty -m 'fix: quick patch'
git switch main

Output:

Warning: you are leaving 1 commit behind, not connected to
any of your branches:

  8b2e4d0 fix: quick patch

If you want to keep it by creating a new branch, this may be a good time
to do so with:

 git branch  8b2e4d0

Switched to branch 'main'

The commit made in detached HEAD is not deleted immediately, but it is unreachable from any branch, so it will eventually be garbage collected. Fix: before switching away, create a branch to hold the work with git switch -c <name>. If you already switched away, Git’s warning gives you the SHA, so you can still recover it:

git switch -c hotfix/quick-patch 8b2e4d0

Output:

Switched to a new branch 'hotfix/quick-patch'

Mistake 3: Discarding work with checkout — .

git checkout -- .

Run in a directory with unstaged edits you actually wanted to keep, this instantly replaces every changed tracked file with the version already in the index — with no confirmation and no easy undo, since the previous working-tree content was never committed anywhere. Fix: run git status and git diff first to see exactly what you are about to lose, restore specific paths instead of . when unsure, and use git stash if you might want the changes back later.

Best Practices

  • Default to git switch for branches and git restore for files; read git checkout comfortably, since it appears constantly in older material, but avoid writing new scripts around its overloaded behavior.
  • Run git status before switching branches so you know in advance whether uncommitted changes will block the switch.
  • Use git switch -c <branch> <start-point> to branch off a specific commit, tag, or another branch, rather than always branching from whatever you happen to be on.
  • Name branches descriptively with a type prefix, such as feature/login-page or fix/nav-bar-overflow, mirroring the Conventional Commits style (feat:, fix:, chore:) you use in commit messages.
  • Before leaving a detached HEAD, create a branch with git switch -c <name> if you made any commits there worth keeping.
  • Avoid reaching for -f/--force out of habit; understand that it discards local changes with no built-in undo, so commit or stash first.
  • Use git switch - to jump back to whatever branch you were previously on.
  • Run git fetch before switching onto a teammate’s branch, so you are not working from a stale remote-tracking tip.

Practice Exercises

  • Starting from main, create a branch called chore/update-deps, make and commit a small change, then switch back to main and confirm the change is not present there. (Hint: git switch -c, then compare with git log --oneline on each branch.)
  • Check out an older commit from your log directly, so you land in detached HEAD, make an experimental commit, then decide you want to keep it. Attach a branch to it without losing the commit.
  • Modify a tracked file without staging it, note its contents with git diff, then discard the change with git restore <file>. Confirm the working tree afterward exactly matches the last commit.

Summary

  • A branch is a movable pointer to a commit; HEAD normally points at a branch, and switching branches moves HEAD, not history.
  • git switch and git checkout <branch> both update the working tree and index to match the target commit’s tree, then repoint HEAD.
  • git switch -c <branch> (or git checkout -b <branch>) creates and switches to a new branch in one step.
  • Checking out a raw commit or tag instead of a branch produces detached HEAD state; commits made there need a branch to stay reachable.
  • Git 2.23 split checkout‘s branch-switching and file-restoring roles into git switch and git restore to remove ambiguity — prefer them for new work.
  • Git refuses to switch branches over uncommitted changes that would be overwritten; commit, stash, or use --force/--merge deliberately.
  • git checkout -- <path> and git restore <path> discard unstaged edits to a file immediately and without an easy undo.