Haskell Newtype Patterns That Prevent Real Production Bugs
A payments service I helped debug once sent a refund of 4,200 to the wrong customer. The function took two arguments, a customer ID and an order ID, both plain Int.

A payments service I helped debug once sent a refund of 4,200 to the wrong customer. The function took two arguments, a customer ID and an order ID, both plain Int. Someone swapped them at a call site, the types agreed, the compiler stayed silent, and the bug shipped. Haskell newtype patterns exist for exactly this kind of mistake, and they cost almost nothing at runtime.
This article walks through the newtype patterns I reach for in production Haskell: wrapping identifiers, smart constructors, units of measure, phantom tags and deriving strategies. Each section shows where the pattern pays off and where it turns into ceremony that slows a team down.
What a Haskell Newtype Actually Gives You
A newtype declaration such as newtype CustomerId = CustomerId Int creates a brand new type with exactly one constructor and exactly one field. At compile time, CustomerId and Int are different types, so passing one where the other is expected is a type error. At runtime, GHC erases the wrapper completely. There is no extra pointer, no extra allocation, no boxing beyond what Int already had.
That last point is what separates newtype from a single field data declaration. A data type with one field adds a layer of laziness: the wrapper itself can be bottom while the inner value is fine. A newtype cannot, because there is no wrapper at runtime to be undefined. In practice this means you can wrap hot path values without measuring a performance change, and I have never seen a benchmark where switching data to newtype made things slower.
Wrapping Identifiers Before They Collide
The refund bug above is the most common case, and the fix is boring in the best way. Give every identifier its own newtype: CustomerId, OrderId, InvoiceId. A function signature like refund :: CustomerId -> OrderId -> Money -> IO () now documents itself and refuses swapped arguments.
Three rules keep this from becoming tedious:
- Wrap at the boundary. Convert raw Int or Text into the newtype where data enters the system, in the JSON decoder or the database row parser, and unwrap only where it leaves.
- Derive the boring instances. Eq, Ord and Show cover most identifiers. Add Hashable if the ID becomes a map key.
- Do not derive Num for identifiers. Adding two customer IDs is meaningless, and a Num instance lets that nonsense compile.
In a codebase of around 40,000 lines, this change touched roughly 300 call sites for us and surfaced two real argument swaps that tests had missed for months.
Smart Constructors for Values With Rules
Some values are not just labelled, they are constrained. An email address must contain an at sign. A percentage must sit between 0 and 100. A port number must fit in 16 bits. The smart constructor pattern hides the real constructor and exports a function that validates first.
You write newtype Percent = Percent Double, export the type but not the constructor, and provide mkPercent :: Double -> Maybe Percent. Any code holding a Percent can trust it without checking again. If returning Maybe feels familiar, it is the same idea covered in how the Maybe type replaces null in Haskell, applied to validation instead of absence.
When Maybe is not enough
For user facing input, swap Maybe for Either with an error type, so the form can say "percentage above 100" rather than a vague failure. Keep the error type small, three to five constructors, or callers will stop matching on it.
Units of Measure Without a Library
Mixing seconds and milliseconds is a classic. A timeout configured as 30 that the HTTP client reads as 30 microseconds is not hypothetical; GHC's own threadDelay takes microseconds, and plenty of people have passed it seconds.
Plain newtypes handle most of this. Define Seconds, Milliseconds and Microseconds, each wrapping an Int, and write explicit conversion functions between them. Derive Num here if you like, because adding two durations in the same unit is meaningful. What you cannot do by accident is add Seconds to Milliseconds.
| Approach | Safety | Cost to adopt |
|---|---|---|
| Raw Int everywhere | None | Zero |
| One newtype per unit | Catches unit mixing | An afternoon |
| Type level units library | Catches derived units too | Days, plus slower compiles |
For most services the middle row is the sweet spot. Type level unit libraries are impressive, but I have only seen them earn their keep in physics and finance code where derived units like metres per second squared actually appear.
Phantom Types for State You Cannot See
A phantom type parameter appears in the type but not in the value. newtype Token (s :: Status) = Token Text lets you write Token Unverified and Token Verified as distinct types over the same text. A function verify :: Token Unverified -> IO (Maybe (Token Verified)) becomes the only path to a verified token, and any function that needs one says so in its signature.
I use this for exactly three things: sanitised versus raw strings, validated versus unvalidated config, and authenticated versus anonymous request contexts. Each one replaced a boolean field that someone eventually forgot to check. If the phantom pattern feels close to type class tricks, the background in understanding type classes in Haskell with real examples helps, since you will often pair phantom tags with class constraints.
Why You Should Not Newtype Everything
Common advice in Haskell circles says to wrap every primitive. I disagree, and I have watched it hurt teams. When every Text is a distinct newtype, a simple function that formats a log line needs six unwrap calls, and new hires spend their first week fighting conversions instead of shipping.
My rule of thumb: wrap a value when two values of the same underlying type travel together through the same functions, or when the value carries an invariant. A customer's display name that only ever goes to a template does not need a newtype. Two IDs passed side by side do. A field that must be non empty does.
The other cost is instance boilerplate. If you find yourself hand writing ToJSON, FromJSON, ToField and FromField for twenty wrappers, you have crossed into ceremony. That is where deriving strategies come in.
Deriving Strategies That Keep Newtypes Cheap
GHC gives you three deriving strategies, and choosing the right one removes most of the boilerplate.
- stock derives Eq, Ord, Show and friends the classic way. Show will print the constructor name, which is useful in logs.
- newtype reuses the underlying type's instance directly. Deriving ToJSON via the newtype strategy means a CustomerId serialises as a bare number, which is almost always what an API wants.
- via borrows an instance from any representationally equal type. It is ideal for reusing a shared instance, for example making every ID type serialise with a prefix by deriving via a single helper type.
Turn on DerivingStrategies and DerivingVia, and write the strategy explicitly on each deriving clause. Explicit strategies make code review faster because nobody has to guess which instance GHC picked. For more on the core ideas behind this style, the programming section collects the related pieces on purity and effects.
Frequently Asked Questions
Does a Haskell newtype have any runtime cost?
No. GHC erases newtype wrappers during compilation, so a CustomerId has the same memory layout as an Int. The coerce function even converts lists of newtypes to lists of the underlying type without traversing them.
Should I use newtype or a type synonym?
Use newtype when you want the compiler to reject mixups. A type synonym like type CustomerId = Int is only a nickname, so the swapped argument bug from the opening would still compile.
How do I add newtypes to an existing codebase gradually?
Start with the function that has the most arguments of the same primitive type. Wrap those, fix the compile errors, and merge. Repeat weekly. Each round is small enough to review and usually exposes at least one latent bug.
A Practical Starting Point
Open your codebase today and search for function signatures with two or more Int, Text or UUID arguments in a row. Pick the one that handles money or permissions, wrap its identifiers in newtypes with explicit deriving strategies, and push the conversions to the edges. One afternoon of work buys you a class of bugs that simply stops compiling.
More in Programming
Programming
Haskell foldr vs foldl and When to Use Each One
You wrote a fold over a list of ten million numbers, it worked in GHCi on a small input, and then the real run ate 1.5 GB of...
Programming
Haskell Record Syntax Without the Field Name Headaches
You add a second record type with a field called name , and GHC refuses to compile because the first record already owns that...
Programming
How the Maybe Type Replaces Null in Haskell
Most mainstream languages let any reference be null, which means any value might secretly be absent, and the only way to find...