Skip to content
ExponaBLOG
GIT & GITHUB FOR PROFESSIONAL DEVELOPERS · DAY 013 / 100

Resolve Git conflicts: a practical guide for developers

Fix merge conflicts calmly.

Expona Admin6 min read

Why conflicts happen — Two branches changed the same lines

Merge conflicts are one of the most common points of friction in team development. They happen because Git tracks changes at the line level, and when two branches modify the same lines in the same file, Git cannot automatically decide which version to keep. Understanding why they occur is the first step to handling them calmly and efficiently.

Picture this: you're working on a feature branch that updates authentication logic in src/auth.ts. Meanwhile, a teammate on another branch fixes a security bug in the exact same section of code. When you try to merge these branches, Git sees conflicting edits and stops the merge process, asking you to resolve the problem manually.

Conflicts arise in three main scenarios:

  • Both branches edit the same line. The most direct conflict—two people changed the same line differently.
  • One branch deletes a line another branch modifies. Git cannot decide whether to keep or remove the line.
  • Structural changes in the same region. Refactoring or reorganizing code in overlapping areas triggers conflicts even if the actual line numbers differ slightly.

In production teams, conflicts are not failures or mistakes. They are signals that your team is working in parallel and that communication or coordination may need adjustment. The key is to resolve them systematically without losing work or introducing bugs.

Read the markers — Understanding Git's conflict notation

When Git encounters a conflict, it marks the conflicting sections in the file itself using three special markers. Learning to read these markers quickly is essential because they tell you exactly what Git sees and which version comes from where.

Open any conflicted file and you'll see a structure like this:

bash
<<<<<<< HEAD
const apiUrl = 'https://api.prod.example.com';
=======
const apiUrl = process.env.API_ENDPOINT;
>>>>>>> feature/environment-config

Let's break down each marker:

  • <<<<<<< HEAD — The start of the conflict. Everything from here until the equals signs is from your current branch (the one you're merging into).
  • ======= — The divider. Everything below this until the final marker is from the incoming branch (the one you're merging in).
  • >>>>>>> feature/environment-config — The end of the conflict. The branch name shown is the source of the incoming changes.

In a real scenario with a larger conflict, you might see multiple sections marked this way in the same file, especially if the same region was edited several times across different parts of the codebase.

Before you edit, take a moment to understand what each side does. Look at the context—variable names, function signatures, comments—to understand the intent. Ask yourself: which change is correct? Should we keep both? Do they need to be combined?

Resolve and continue — Edit, stage, and move forward

Resolution is not mysterious. You manually edit the file, remove the markers, keep the code you want, and tell Git you're done. Then you stage the file and continue the merge or rebase.

Let's walk through a realistic example. Suppose you have a database configuration file with a conflict:

bash
<<<<<<< HEAD
const dbConfig = {
  host: 'localhost',
  port: 5432,
  pool: { max: 10 }
};
=======
const dbConfig = {
  host: process.env.DB_HOST,
  port: process.env.DB_PORT,
  pool: { max: parseInt(process.env.DB_POOL_MAX || '20') }
};
>>>>>>> feature/env-config

In this case, the feature branch introduced environment variable support, which is the better approach for production systems. You decide to keep the incoming changes. Simply delete the conflict markers and the old code:

bash
const dbConfig = {
  host: process.env.DB_HOST,
  port: process.env.DB_PORT,
  pool: { max: parseInt(process.env.DB_POOL_MAX || '20') }
};

Other times, you might need to blend both versions. Imagine a conflict in a React component where HEAD added error handling and the incoming branch added new props:

bash
<<<<<<< HEAD
function UserForm({ name, email, onSubmit }) {
  const [error, setError] = useState('');
  // error handling logic
}
=======
function UserForm({ name, email, phone, department, onSubmit }) {
  // form fields
}
>>>>>>> feature/extended-fields

The resolution here combines both: keep the new props and the error handling:

bash
function UserForm({ name, email, phone, department, onSubmit }) {
  const [error, setError] = useState('');
  // error handling logic for all fields
}

Once you've resolved the conflict, stage the file:

bash
git add src/config.ts

If you're in the middle of a merge, continue with:

bash
git merge --continue

If you're in the middle of a rebase, continue with:

bash
git rebase --continue

Git will then prompt you for a commit message (if it's a merge) or move to the next commit in the rebase. From here, the operation proceeds as normal. Crucially, always verify your changes before committing. Run your tests, check the syntax, and make sure the logic still makes sense after the merge.

Abort if needed — Know your escape route

Sometimes during a merge or rebase, you realize the conflict is more complex than expected, or you need to involve a teammate. Git offers a safe way out: the abort command.

If you're mid-merge and want to stop:

bash
git merge --abort

This returns your working directory to the state before you ran git merge. No changes are lost; you simply exit the merge process.

Similarly, if you're rebasing:

bash
git rebase --abort

This is especially valuable when a rebase encounters multiple conflicts across several commits. If the conflicts become overwhelming or you realize the merge strategy is wrong, abort and regroup. You might switch branches, consult with teammates, or choose a different merge strategy entirely.

Using abort is not a failure. In professional teams, it's often the smart choice. For instance, if you begin a merge and discover that the conflicts touch critical business logic, aborting lets you pause, review the changes with a domain expert, and come back with a clearer picture.

Common mistakes

Mistake 1: Resolving conflicts without understanding the context. A developer might blindly delete one side of a conflict marker to make the conflict go away. Always read both versions and understand what they do. A two-minute conversation with the teammate who made the other change can save hours of debugging later.

Mistake 2: Forgetting to test after conflict resolution. Once you've manually edited a file during conflict resolution, the syntax might be broken or the logic might be flawed. Run your test suite, linter, and type checker before pushing. Conflicts resolved incorrectly slip into production more often than you'd expect.

Mistake 3: Not staging the resolved file. Some developers resolve conflicts but then forget to git add the file before continuing. Git will think the file is still in conflict and won't let you proceed. Always stage after resolving.

Mistake 4: Merging without pulling the latest version first. Pull the target branch before merging into it. This often prevents conflicts altogether because you're already up to date. A quick git pull origin main before git merge feature/my-feature saves time.

Key takeaways

  • Conflicts happen when two branches edit the same lines. They're normal and signal parallel work, not mistakes.
  • Git's conflict markers (<<<<<<<, =======, >>>>>>>) show both versions clearly. Take time to understand each side before editing.
  • Resolve by editing the file, keeping the version(s) you need, removing the markers, staging, and continuing with git merge --continue or git rebase --continue.
  • Use git merge --abort or git rebase --abort if the conflict is complex or you need to regroup. This is always safe and clears the conflict state.
  • After resolving, run tests and linters to ensure the merged code is correct. Conflicts can introduce subtle bugs if not carefully reviewed.
  • Pull the target branch before merging to reduce conflicts. Communication with teammates also prevents many conflicts upfront.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles