Fixing Cabal's "Could Not Resolve Dependencies" Error
You clone a Haskell project that built fine for its author last spring, run cabal build, and get a wall of text that starts with "Could not resolve dependencies" and ends with "After searching the rest of the dependency tree exhaustively,...

You clone a Haskell project that built fine for its author last spring, run cabal build, and get a wall of text that starts with "Could not resolve dependencies" and ends with "After searching the rest of the dependency tree exhaustively, these were the goals I've had most trouble fulfilling". In between are dozens of lines of "rejecting" and "conflict". Most people scroll to the bottom, see a package name, and start editing bounds at random.
The message is less hostile than it looks. Cabal's solver is telling you, in order, which combination of versions it tried and exactly which constraint ruled each one out. Once you know how to read three or four of those lines, most of these errors take minutes to fix, and you will fix them in a way that does not come back to bite you on the next machine.
What the solver is actually telling you
Cabal tries to pick one version of every package in your build plan so that every version bound in every .cabal file is satisfied at once. When it cannot, it prints a trace of its search. The lines worth reading look something like this in shape:
[__0] trying: myapp-0.1.0.0 (user goal)
[__1] next goal: base (dependency of myapp)
[__1] rejecting: base-4.19.1.0/installed (conflict: myapp => base>=4.14 && <4.18)
Read the first rejection, not the last. The number in brackets is the depth of the search. The rejection at the shallowest depth is usually the real cause, and everything after it is the solver backtracking through alternatives that were never going to work. In the example above, the project asks for a version of base below 4.18, but the compiler on this machine ships base 4.19. No amount of fiddling with other packages will help.
The phrase "/installed" matters too. It means the version came with your GHC and cannot be swapped out. Packages like base, ghc-prim and template-haskell are tied to the compiler, which is the single most common source of these errors.
The base problem, and the compiler behind it
Because base is bundled with GHC, a bound on base is really a bound on which compilers a package supports. When a project's author wrote "base >=4.14 && <4.18", they were saying "GHC 9.4 or older", whether they thought of it that way or not.
| base version | Ships with GHC |
|---|---|
| 4.17 | 9.4 |
| 4.18 | 9.6 |
| 4.19 | 9.8 |
| 4.20 | 9.10 |
| 4.21 | 9.12 |
If the conflict is on base, your first and cleanest option is to build with the compiler the project expects. With GHCup installed, ghcup tui lets you install and switch GHC versions in a few keystrokes, or you can tell cabal which compiler to use for one build with the -w flag pointing at a specific ghc binary. Our guide to setting up a Haskell development environment covers getting GHCup in place if you have not already.
If you own the project and want it to support your newer compiler, raise the upper bound on base, build, and run the tests. That is a real code change, not a workaround, and it belongs in a commit.
When the conflict is not base
If the shallow rejection names an ordinary library, the fix depends on whose bound is in the way.
Your own bound is too tight
Check the package's own .cabal file for the dependency in question. Many projects use the caret form, such as "aeson ^>=2.1", which means at least 2.1 and below 2.2. If another dependency needs aeson 2.2, widen your bound to cover both, build, and test. The cabal gen-bounds command can suggest bounds based on what currently builds, which is a helpful starting point.
A dependency's bound is too tight
This is the awkward case: a library you do not control says it does not work with a version that, in practice, it probably does. Before you override anything, run cabal update. A surprising share of these errors come from a stale package index on your machine, and Hackage maintainers and trustees regularly publish metadata revisions that relax bounds without a new release. Your old index simply has not seen them.
If the bound is still there after updating, check the library's issue tracker. Somebody has often already reported it, and the comment thread will tell you whether the newer version really works.
Why allow-newer everywhere is a trap
The most common advice on forums is to add allow-newer: all to your cabal.project and move on. It does make the error disappear. It does so by telling the solver to ignore upper bounds across your whole dependency tree, which means you lose the one mechanism that warns you when two libraries genuinely do not fit together.
The failure mode is not dramatic. The build usually succeeds. Then, weeks later, you hit a type error deep inside a dependency, or worse, a change in behaviour that no type error catches, and you have no record of which bound you overrode to get there.
Override the one bound you have checked, not every bound you have not.
If you do need an override, scope it. In cabal.project you can write an allow-newer line for a single pair, such as a specific library and the dependency it is too strict about, and add a comment explaining why and linking the upstream issue. That leaves the rest of the solver's checks intact, and the next person to read the file knows when the line can come out. The same applies to constraints lines that pin a version: one line, one reason, one comment.
It also helps to understand why upper bounds exist at all. The Package Versioning Policy that most of Hackage follows is a social agreement about what version numbers promise, and our piece on how the Haskell community builds and maintains its packages explains where it came from and where people disagree about it.
Getting a more useful error next time
Two habits make these failures easier to diagnose. First, run cabal build with the dry-run flag before a real build when you change dependencies; you get the solver's verdict without waiting for compilation. Second, if the default output is not enough, add -v3 to see the full search. It is long, but searching it for the first "rejecting" line is usually faster than guessing.
Once a project builds, cabal freeze writes the exact versions to cabal.project.freeze. Committing that file means the next machine, and the next contributor, gets the same plan instead of rediscovering the conflict. We list it among the Haskell tools worth keeping close for exactly that reason.
This week, take one Haskell project you have not built in a few months, run cabal update and then a dry-run build. If it fails, find the first rejection line and decide which of the cases above it is. If it succeeds, run cabal freeze and commit the file, so the next person never sees the error at all.
More in Tutorials
Tutorials
Why Pure Functions Make Debugging Dramatically More Predictable
Every programmer has spent an afternoon staring at a function that behaves differently depending on when it is called, what ran...
Tutorials
Understanding Type Classes in Haskell With Real Examples
Type classes are one of Haskell's most powerful and distinctive features.
Tutorials
Getting Started With Haskell for Complete Beginners
Haskell has a reputation for being difficult to learn.