Haskell World logo Haskell WorldWrite code, build worlds
Tutorials

How to Read GHC Error Messages Without Panicking

The first time most people compile a twenty line Haskell program, GHC answers with forty lines of complaint.

Terminal showing a GHC error message next to the Haskell source line it points to

The first time most people compile a twenty line Haskell program, GHC answers with forty lines of complaint. One missing argument can produce a message that mentions rigid type variables, ambiguous constraints and a function you never wrote. I have watched students close the terminal at that point and decide the language is not for them, when the actual fix was adding a single pair of parentheses.

Reading GHC error messages is a skill you can practise like any other, and it pays off faster than almost anything else in your first month. This tutorial walks through the shape of a typical error, the five messages beginners meet most often, and a reading order that gets you to the real problem in under a minute most of the time.

The Anatomy of a GHC Error Message

Every GHC error follows the same skeleton, and once you see it the wall of text breaks into pieces. The first line gives the file, the line and column, and since GHC 9.6 a bracketed code such as GHC-83865 that you can look up in the official error index.

After the location come three layers:

  • The headline. Usually one sentence: "Couldn't match expected type" or "Variable not in scope". This is the only part you must read carefully.
  • The detail. Which types GHC expected and which it actually found, plus where those types came from.
  • The context. Lines starting with "In the expression" or "In an equation for". These tell you where GHC was looking, which is not always where you made the mistake.

Beginners tend to read top to bottom and get lost in the context. Read the headline, then jump straight to the line and column, then come back for the detail only if the fix is not obvious.

Couldn't Match Expected Type

The most common message of all says GHC needed one type at a certain position and you supplied another. The detail lines will read something like "Expected: Int, Actual: Maybe Int" or "Expected: String, Actual: [Char] -> String".

Two patterns cover the large majority of cases. The first is a value wrapped in something you forgot to unwrap: a Maybe, an IO action or a list. If you are fighting Maybe specifically, the walkthrough on how the Maybe type replaces null covers the pattern matching that resolves it. The second pattern is a function where a value was expected, which nearly always means a missing argument or a missing pair of parentheses around an application, as in print length xs instead of print (length xs).

No Instance for a Type Class

"No instance for (Show Foo) arising from a use of print" sounds abstract, but it is the most literal message GHC produces. You asked to print a Foo, and nobody told the compiler how to turn a Foo into text. Adding deriving (Show) to the data declaration fixes it in about nine cases out of ten.

The harder variant is "No instance for (Num [Char])". Here GHC is telling you that you used a number where a string lives, or the reverse, and it tried to make your string behave like a number. The class in the message is a clue to the operation, not to the fix. Num means arithmetic happened somewhere, Eq means a comparison, Ord means sorting or a less than check. If type classes still feel slippery, this guide to type classes with real examples builds the mental model that makes these messages readable.

Variable Not in Scope and Ambiguous Types

Variable not in scope

This one is mostly typos and missing imports. GHC helpfully suggests names that are close, so "Variable not in scope: lenght" comes with "Perhaps you meant length". When there is no suggestion, check your imports: sortBy lives in Data.List, not the Prelude.

Ambiguous type variable

The classic trigger is read or show . read with nothing that pins down the type. GHC knows you want something readable but cannot choose between Int, Double and dozens of others. The fix is a type annotation at the point of use, such as read line :: Int. You do not need to annotate the whole function, only the spot where the choice is made.

Why You Should Not Just Add Type Signatures Everywhere

The standard advice on forums is "add type signatures to every top level function and the errors get better". That advice is half right, and the wrong half wastes beginners a lot of time.

Signatures do help GHC report errors closer to where you made them. But a signature you guessed is a second source of mistakes. I regularly see learners write average :: [Int] -> Int, use division inside, and then spend twenty minutes on a "No instance for (Fractional Int)" error that their own signature created. Without the signature, GHC would have inferred a perfectly sensible type.

A better habit: write the function first, load it into GHCi, and ask for its type with :t. If the inferred type matches what you meant, paste it in as the signature. If it does not, the difference is exactly the bug you were about to have. The article on using GHCi as your daily workflow shows how to make that loop take seconds.

A Four Step Reading Order That Works

When a message lands, run through this in order and stop as soon as the fix appears:

  1. Read only the headline. Classify it: type mismatch, missing instance, scope, ambiguity or something else.
  2. Go to the exact column. GHC column numbers are accurate. Put your cursor there and look at the single expression it points to.
  3. Compare expected and actual. Say out loud what the difference is. "It wanted a String and I gave it a Maybe String" is usually the whole answer.
  4. Read the context last. Only if steps one to three did not resolve it, read the "In the expression" lines to see how GHC reached its conclusion.

One more rule: fix the first error only, then recompile. GHC errors cascade, and a single mismatch on line 12 can produce four more messages further down that vanish once the first is fixed.

Tools That Make Errors Shorter

A few settings cut the noise noticeably.

Tool or flagWhat it changes
Haskell Language ServerShows the headline inline at the exact position as you type
-fdiagnostics-color=alwaysHighlights expected and actual types so the difference stands out
-freverse-errorsPrints the first error last, right above your prompt
Typed holes (an underscore)Asks GHC what type belongs in a spot, with suggestions

FAQ

Why does GHC point to a line that looks correct?

Type inference works across a whole function, so the place where GHC notices a conflict is not always where you introduced it. Check the other uses of the same variable, and add a temporary type annotation to narrow it down.

Are GHC error messages better in newer versions?

Yes. Versions from 9.4 onward added error codes, clearer wording for common cases and better suggestions. If you are on something older than 9.2, upgrading through GHCup is worth it for the messages alone.

Should I search the full error text online?

Search the error code or the headline, never the whole message. The full text contains your own names and types, which makes the search match nothing useful.

Turning Errors Into a Habit

For the next week, every time GHC rejects your code, write down the headline in a text file before fixing it. By day five you will notice that the same three or four messages make up nearly the whole list, and you will start fixing them before you finish reading. That is the moment the compiler stops feeling like a critic and starts feeling like a careful reviewer.

AS
Adil Sato

Adil Sato teaches programming concepts at a community college and writes online guides for the questions students ask most in the second week of class, not the first. His focus is on making type systems, monads, and functional patterns understandable without reducing them to metaphors that break down the moment you try to use them for real work.

More posts by Adil

More in Tutorials

01doorganico 084goad 3hot8 4465456 after5internprogram ahandfulofblessings ahmadpc ahomcybersecurity allodialresources annodex antiquesandperiodcollectibles aotc asianworldkashmir beedlectrics bloggertemplatefree bluefeatheragency bodyweightsolution campaign08blog carfixadvisor catstreetcollective cbstvsports cnfanspurchasing committeeof500yearsofdignityandresistance covertopsdjs cs-kaiyuangame cuestacomunicaciontotal cxoutlet dancebetgame deskgearhub destinyvillastay detectmagazine di33 diamondsteelbuildings donmo dyanadropship emarsysfinance emimattl escapechan eskitarih evautoplay everythingweddings-orlando farmzone feedviewerapp foxstudiosscratch gamevui123 gleasongrade greetingsday hacklabasu iw-kaiyun jagoanproperti jilidskripsi joltbusinessfinance juegodepokergratis juegospucca kaeru-festival latestmehndidesigns lestanzediottaviano litemagazines localcommunityhero lustrsazan mariashebets menswheels newwebstechs1 nikkit ootheday oxigenvpn plurfitness prefetial probudgetcarpetcleaning psychicmrsmartin pyaten qvantor rejoignezlogis rexzbot riselayouts ruchin rwxqsgd5 sakaltime shibiaowang shop-bot skrtips strcoding tetapatrium topselller uneplanteartificielle vipertechcr webtech1223411 wo-kygaming yamaha-mekar yeknava yoybuysheets zenunwa yamaha-mekar jilidskripsi ankertoto ankertoto ankertoto ankertoto