Skip to content
AgileLogics
5 min read

How Haskell Code Runs

Prerequisites: Your first program

When you compile or run a Haskell file, GHC works in stages:

  1. Parse the file. Syntax errors — like bad indentation — are reported now.
  2. Type-check every expression. Most mistakes are caught here, before the program ever runs.
  3. Compile to native machine code (with ghc) or to bytecode that the interpreter runs (with runghc and ghci).
  4. Run main, evaluating expressions lazily as their values are needed.

A type error

main :: IO ()
main = do
  putStrLn "start"
  putStrLn ("Total: " ++ 42)
main.hs:4:26: error: [GHC-39999]
    * No instance for `Num [Char]' arising from the literal `42'
    * In the second argument of `(++)', namely `42'
      In the first argument of `putStrLn', namely `("Total: " ++ 42)'
      In a stmt of a 'do' block: putStrLn ("Total: " ++ 42)
  |
4 |   putStrLn ("Total: " ++ 42)

  |                          ^^

Nothing ran — not even "start" — because the file failed to type-check. ++ joins two lists, and 42 is a number, not a string. The fix is "Total: " ++ show 42. Read error messages carefully: they give the file, line, and column (4:26), and a code like GHC-39999 you can look up on errors.haskell.org.

A runtime error

Type checking can't catch everything:

main :: IO ()
main = do
  putStrLn "start"
  print (head ([] :: [Int]))
  putStrLn "never reached"
main.hs:4:10: warning: [GHC-63394] [-Wx-partial]
    In the use of `head'
    (imported from Prelude, but defined in GHC.Internal.List):
    "This is a partial function, it throws an error on empty lists. Use pattern matching, 'Data.List.uncons' or 'Data.Maybe.listToMaybe' instead. Consider refactoring to use "Data.List.NonEmpty"."
  |
4 |   print (head ([] :: [Int]))

  |          ^^^^

start
main.hs: Uncaught exception ghc-internal:GHC.Internal.Exception.ErrorCall:

Prelude.head: empty list

HasCallStack backtrace:
  error, called at libraries\ghc-internal\src\GHC\Internal\List.hs:1969:3 in ghc-internal:GHC.Internal.List
  errorEmptyList, called at libraries\ghc-internal\src\GHC\Internal\List.hs:96:11 in ghc-internal:GHC.Internal.List
  badHead, called at libraries\ghc-internal\src\GHC\Internal\List.hs:90:28 in ghc-internal:GHC.Internal.List
  head, called at main.hs:4:10 in main:Main

This time the program compiled — with a warning first — then "start" printed, and the program crashed: head of an empty list has no answer. The backtrace at the end shows the chain of calls that led to the error, ending with your own code (main.hs:4:10). You'll learn to avoid partial functions like head by using pattern matching and Maybe instead — which is exactly what GHC's -Wx-partial warning suggests.

Purity in one sentence

Code outside IO can't print, read input, or change anything — it can only compute a result from its arguments. That's why the compiler can rearrange, share, and lazily evaluate it safely, and why functions are so easy to test.