Modeling Network Responses Safely with Haskell's Algebraic Data Types
Learn how Haskell's Algebraic Data Types let you model network responses with compile-time safety, preventing runtime errors through exhaustive pattern matching.
07 Jan 2026, 09:00 UTC

The Problem: Handling Network Responses Without Runtime Errors
When building networked applications, handling responses safely is critical. A common mistake is using nullable types or stringly-typed error handling, which leads to runtime crashes when unexpected values appear. In Haskell, we can eliminate this class of errors entirely using Algebraic Data Types (ADTs).
Thesis: ADTs Provide Exhaustive, Compile-Time Error Handling
By modeling a network response as an ADT with explicit constructors for success and failure, we force ourselves to handle every possible case at compile time. This prevents the "it works on my machine" problem where error paths are forgotten until production.
Concrete Example: A Network Response ADT
Let's model a simple HTTP response that can either succeed with data or fail with an error message:
data NetworkResponse a = Success a | Error String deriving (Show, Eq)
-- Example usage with a user type
data User = User { userId :: Int, userName :: String } deriving (Show, Eq)
-- Simulate a network call
fetchUser :: Int -> IO (NetworkResponse User)
fetchUser id = do
let result = if id > 0
then Success (User id "Alice")
else Error "Invalid user ID"
return result
-- Pattern matching handles both cases explicitly
handleResponse :: NetworkResponse User -> String
handleResponse (Success user) = "Got user: " ++ userName user
handleResponse (Error msg) = "Error: " ++ msg
-- Main program
defined in IO monad
main :: IO ()
main = do
response <- fetchUser (-1)
putStrLn $ handleResponse response
-- Output: Error: Invalid user ID
This example demonstrates how the compiler will refuse to compile code that doesn't handle both Success and Error cases. No runtime exceptions, no null pointer crashes.
Trade-offs and Limitations
While ADTs provide excellent safety, they require upfront design work. You must anticipate all possible states your data can be in. For rapidly evolving APIs, this might feel restrictive initially. However, this friction is beneficial—it forces you to think through edge cases before they become bugs in production.
Additionally, ADTs work best when the set of cases is closed. If you need to dynamically add new error types, you might need to extend your sum type or use a different approach like Either from Data.Either for simpler two-case scenarios.
Actionable Next Steps
Try this yourself: create a NetworkResponse type for your own domain (perhaps API responses from a service you use). Write a function that processes this response using pattern matching. Notice how GHC's warning system helps you catch missing cases before runtime.
The key insight is that Haskell's type system doesn't just catch type errors—it catches logic errors in your business domain. By modeling your problem domain as ADTs, you shift error detection from runtime to compile time, where it's cheaper and safer to fix.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.