- Courses
- đ»Programming Languages
- đ«OCaml (WIP)
- Advanced Type System
- Extensible Variants and Exn Patterns
On this page
OCaml Extensible Variants and Exn Patterns
Published on September 15, 2026
By Enrico Hochkoeppler
Web Developer & TutoriaLibre Founder
Extensible variant types, introduced in OCaml 4.02, allow a variant type to be extended with new constructors after its initial definition. The built-in exn type is the most familiar example: every exception declaration adds a constructor to exn at compile time, and the type remains open to further extension. Understanding extensible variants clarifies how exceptions work under the hood, why pattern matching on exn requires a default case, and how to design libraries that accept user-defined constructors without modifying the library itself.
What Makes a Variant Extensibleâ
A regular variant type has a fixed, closed set of constructors determined entirely at its definition site. Once the type is defined, no other module can add constructors to it. An extensible variant type, by contrast, is declared with .. as its definition and then extended with += wherever new constructors are needed.
type t = ..
The .. syntax declares an empty extensible variant type. At this point t has no constructors. Constructors are added later, often in different modules or even different compilation units.
type t += A
type t += B of int
type t += C of string * float
Each type t += ... declaration extends t with one or more new constructors. The constructors behave like ordinary variant constructors: they can carry arguments, they can be pattern-matched, and they participate in the same type. The crucial difference is that the set of constructors is open. Code that matches on values of type t must account for the possibility that new constructors have been added since the code was written.
This openness is what makes extensible variants useful for library design. A library can define an extensible type and allow users to add their own cases without forking or modifying the library.
The exn Type as an Extensible Variantâ
The exn type is the most widely used extensible variant in OCaml. Every exception declaration is an extension of exn.
exception NotFound
exception Invalid_argument of string
These declarations are equivalent to the following type extensions:
type exn += NotFound
type exn += Invalid_argument of string
The exn type is predefined with the .. syntax, meaning it is extensible from the start. When a program raises an exception with raise (Invalid_argument "bad input"), it creates a value of type exn whose constructor is Invalid_argument. When that exception is caught with try ... with Invalid_argument msg -> ..., the pattern matches on the constructor and binds msg to the string payload.
Because exn is extensible, pattern matching on it always requires a default case. The compiler cannot know whether all possible exception constructors have been listed. A match or try ... with that handles only specific exceptions must include a catch-all pattern, even if the programmer believes no other exceptions can occur.
Pattern Matching on Extensible Variantsâ
Pattern matching on an extensible variant requires a default case to handle constructors that are not explicitly listed. The compiler enforces this with a warning if the default case is missing.
type attr = ..
type attr += Str of string
type attr += Int of int
let to_string = function
| Str s -> s
| Int i -> Int.to_string i
| _ -> "?"
The _ -> "?" branch handles any constructor that the function does not know about. This is necessary because attr may have been extended elsewhere, and the function cannot enumerate all possible constructors.
The same rule applies to exception handling. A try ... with construct that matches specific exceptions must either include a wildcard pattern or re-raise the exception.
try risky_operation () with
| Not_found -> handle_not_found ()
| Invalid_argument msg -> handle_invalid_argument msg
If the with clause omits the wildcard and the operation raises an exception that does not match any of the listed patterns, the exception propagates outward. The compiler does not warn about this, because propagating unmatched exceptions is a valid design choice. However, if the with clause is intended to be exhaustive over all possible exceptions, it must include a wildcard.
Rebinding Extensible Variant Constructorsâ
Extensible variant constructors can be rebound to a different name. This feature allows a module to export constructors from another module under a new name, which is useful for building layered APIs.
module Expr = struct
type attr = ..
type attr += Str of string
type attr += Int of int
end
type Expr.attr += Str = Expr.Str
The declaration type Expr.attr += Str = Expr.Str creates a new constructor name Str in the current scope that is an alias for Expr.Str. After this declaration, writing Str "foo" creates a value of type Expr.attr, and the constructor is the same as Expr.Str.
Without the rebinding declaration, Str is not in scope. Attempting to use Str "foo" would produce an unbound constructor error.
The rebinding syntax is particularly useful when a library wants to expose constructors from a private or internal module without requiring users to qualify them with the module path. It also allows renaming constructors to avoid name collisions when multiple extensible types share constructor names.
Private Extensible Variant Typesâ
An extensible variant type can be declared private. A private extensible variant prevents new constructors from being added directly by users, while still allowing constructors to be referenced in interfaces and matched in patterns.
module B : sig
type Expr.attr += private Bool of int
val bool : bool -> Expr.attr
end = struct
type Expr.attr += Bool of int
let bool p = if p then Bool 1 else Bool 0
end
In this example, the signature of B declares that Expr.attr has a private constructor Bool of int. Users can pattern-match on B.Bool p to inspect values, but they cannot write B.Bool 1 to construct a value. Construction is only possible through the bool function, which enforces any invariants the module wishes to maintain.
Private extensible variants are declared with = private .. at the type level. This form prevents new constructors from being declared directly in the implementation, while allowing extension constructors to be referred to in interfaces.
Extensible Variants Versus Polymorphic Variantsâ
Extensible variants and polymorphic variants both allow a type to be extended with new cases, but they differ in fundamental ways.
Polymorphic variants, written with backtick-prefixed tags such as `Str "foo", are structurally typed. A function that accepts [< `Str of string | `Int of int ] can accept any value whose tags are a subset of that set. Polymorphic variants do not require a default case in pattern matching if the set of tags is known and closed. They are implemented differently at runtime and do not carry the same identity semantics as extensible variants.
Extensible variants are nominally typed. Each constructor belongs to a specific extensible type, and two constructors with the same name in different types are distinct. Pattern matching requires a default case because the type is open. Extensible variants have a runtime representation that includes the constructor identity, which makes them suitable for cases where constructor identity matters, such as exception handling.
The choice between the two depends on the problem. Polymorphic variants are more flexible for ad-hoc tagging and structural subtyping. Extensible variants are more appropriate when constructors need stable identity, when they must be extended across module boundaries without a predefined signature, or when the type is used as a universal error type like exn.
Exception Patterns in Depthâ
Exception handling in OCaml is pattern matching on values of type exn. The try ... with construct evaluates an expression and, if an exception is raised, matches the exception value against a list of patterns.
try
let x = List.find (fun x -> x > 10) [1; 2; 3] in
x
with
| Not_found -> 0
| Invalid_argument msg -> failwith ("Invalid: " ^ msg)
The with clause can contain any pattern of type exn. This includes patterns that bind values, patterns with guards, and nested patterns. The as pattern can bind the entire exception value while also matching its structure.
try risky () with
| (Failure msg) as exn ->
log_error exn;
default_value
When multiple exceptions need the same handling, they can be combined in a single branch using or-patterns.
try parse input with
| (Invalid_argument _ | Failure _) -> Error "parse failed"
The match expression can also catch exceptions in OCaml 4.02 and later, using the exception pattern. This allows exception handling to be integrated directly into a match expression rather than requiring a separate try wrapper.
match int_of_string_opt s with
| Some n -> n
| None -> 0
| exception Failure _ -> 0
Exception patterns in match are evaluated in order. If the expression raises an exception, the exception patterns are tried. If no exception pattern matches, the exception propagates.
Common Pitfalls and Design Considerationsâ
The most common mistake with extensible variants is forgetting the default case in pattern matching. The compiler produces a warning, not an error, so a program may compile with a default case that silently returns a wrong value. Reviewing warnings and treating them as errors during development prevents this class of bugs.
Another pitfall involves the runtime representation of extensible variant constructors. Each constructor is a runtime value with a unique identity, stored in the module where it is defined. This means an extensible variant cannot be used in a module interface that is compiled without its implementation. The module containing the constructor must be available at link time, because the constructor's identity is a runtime value.
Extensible variants cannot be marshalled safely with Marshal. Marshallling and unmarshalling an extensible variant value loses the identity of its constructor, because identity is based on memory location. For persistent storage or network transmission, regular variants, polymorphic variants, or an explicit serialization scheme are safer choices.
Performance is another consideration. Extensible variant constructors with arguments are represented as blocks with an extra field holding the constructor identity. Pattern matching on extensible variants is slightly slower than on regular variants because the runtime must check the constructor identity rather than a simple tag. For most applications this difference is negligible, but in performance-critical code with millions of matches, regular variants may be preferable.
When designing a library that uses extensible variants, the constructor identity model suggests that the library should provide a way for users to register their own constructors and a way to match on them. The Format module's use of extensible variants for user-defined tags in OCaml 4.08 is a good example: the module defines the extensible type and provides functions for adding and inspecting tags, while users extend the type in their own modules.
Key Takeawaysâ
Extensible variant types allow a variant type to be opened for extension after its definition, using .. to declare the type and += to add constructors. The exn type is the canonical example, and understanding extensible variants illuminates how exceptions work. Pattern matching requires a default case because the set of constructors is open. Constructors can be rebound to new names, and types can be declared private to control construction while allowing inspection. Extensible variants differ from polymorphic variants in their nominal typing and runtime identity semantics, and they have limitations around marshalling and module linking that make them unsuitable for certain use cases. Used carefully, they enable library designs where users extend a type without modifying the library's source code.
Frequently Asked Questionsâ
What is an extensible variant type in OCaml?
An extensible variant type is a variant type that can be extended with new constructors after its initial definition. It is declared with `..` and extended with `+=`. The `exn` type is the built-in example.
Why does pattern matching on extensible variants require a default case?
Because the set of constructors is open. The compiler cannot know whether all possible constructors have been listed, so a default case is required to handle unknown constructors.
Can extensible variant constructors be renamed?
Yes. The syntax `type t += NewName = OldName` creates a rebinding that allows the constructor to be used under a different name in the current scope.
What is the difference between extensible variants and polymorphic variants?
Extensible variants are nominally typed and have constructors with stable runtime identity. Polymorphic variants are structurally typed and use backtick-prefixed tags. Polymorphic variants do not require a default case when the set of tags is closed.
Why can't extensible variants be marshalled?
Extensible variant constructors are distinguished by their memory location. Marshalling does not preserve physical equality, so the constructor identity is lost after a marshal round-trip.
How does the exn type relate to extensible variants?
The exn type is an extensible variant. Every `exception` declaration is a type extension that adds a constructor to exn. This is why exceptions can be pattern-matched with `try ... with` and why a default case is sometimes required.
Found this helpful? Share the knowledge...
... or support this project to keep it alive!
Previous
Recursive Modules
Next
Exercise: Build a Generic OCaml Library with Functors
- What Makes a Variant Extensible
- The
exnType as an Extensible Variant - Pattern Matching on Extensible Variants
- Rebinding Extensible Variant Constructors
- Private Extensible Variant Types
- Extensible Variants Versus Polymorphic Variants
- Exception Patterns in Depth
- Common Pitfalls and Design Considerations
- Key Takeaways
- Frequently Asked Questions
