Incident brief
UNION types cannot be matched
A UNION mixed two branches whose types can't be converted to each other. PostgreSQL resolves UNIONs two branches at a time, so where a mismatch appears in that chain matters.
What lands in your log
ERROR: UNION types text and integer cannot be matched
In 10 seconds
- What triggers it
- UNION an untyped NULL branch with a branch that resolves to text, then UNION the result with an integer branch.
- Fix
- Put a value of the type you actually want in the leftmost branch, so the pairwise resolution locks in early.
- Proof
- Reproduced on PostgreSQL 16.14 → Running the manual's own three-way UNION example was rejected with SQLSTATE 42804. The session was unaffected afterward.
Fix
What to do right now
Application-level steps for this error.
- Put a value of the type you actually want in the leftmost branch, so the pairwise resolution locks in early.
- Or explicitly cast the ambiguous NULL branch to the desired type.
- Don't reach for a blanket cast to make the error disappear, see the premium test for why that can silently change what the query returns.
-- put the integer in the leftmost branch so pairwise resolution locks in early
SELECT 1 UNION SELECT NULL UNION SELECT NULL;For this error
See this error live on the server
Run these against the affected instance to confirm the diagnosis before you act.
UNION types cannot be matched (42804) is statement-local: PostgreSQL does not retain the rejected value after the statement ends. Capture the exact bound value and statement position in application/server logs, then run this targeted validation before retrying.
Compare UNION arm types
Validate the specific value/shape that can raise this SQLSTATE.
-- Run each arm independently with the real expressions.
SELECT pg_typeof(<left_expression>) AS left_type,
pg_typeof(<right_expression>) AS right_type;Why it happens
What PostgreSQL is telling you
The mechanism behind the error, grounded in the official manual, not paraphrased.
PostgreSQL 16 Documentation, §10.5 UNION, CASE, and Related Constructs
The problem can be fixed by ensuring that the leftmost UNION has at least one input of the desired result type.Read the full section on postgresql.org →
The mismatched UNION
PostgreSQL resolves multiple UNIONs as a nest of pairwise operations. The first pair (NULL UNION NULL) resolved to text, PostgreSQL's fallback type for an all-NULL union, and that text result then met an integer in the outer UNION, with no implicit conversion between them.Confirming the session still works
The failed UNION didn't affect the session, the next statement ran normally.Reproduce & verify
A real, single-session PostgreSQL reproduction
A literal transcript of SQL run against a live PostgreSQL instance in an isolated lab. The commands below are exactly what was executed.
- 1UNION an untyped NULL branch with a branch that resolves to text, then UNION the result with an integer branch.
- 2PostgreSQL resolves multiple UNIONs pairwise, left to right, the inner pair locks in a type before the outer pair is checked.
- 3SQLSTATE 42804 is reported once the two sides of a pair can't be matched, naming both types.
One client: UNION three SELECTs where the pairwise-resolved type becomes text before it meets an integer.
-- No table needed: this failure happens purely at SQL type-resolution time.
SELECT 'no schema required' AS setup_note;SELECT NULL UNION SELECT NULL UNION SELECT 1;SELECT 1 AS session_still_alive;What PostgreSQL actually returned
setup_note
--------------------
no schema required
(1 row)ERROR: UNION types text and integer cannot be matched
LINE 1: SELECT NULL UNION SELECT NULL UNION SELECT 1;
^ session_still_alive
----------------------
1
(1 row)The manual's own suggested fix, put a value of the desired type in the leftmost branch, was tested directly against the same three-way UNION shape.
Without this
Above: NULL first, pairwise resolution starts as text and then collides with integer.
With this, tested
Below: the same three SELECTs, integer first, pairwise resolution starts as integer and both NULLs convert to it.
- A second operational test: exact SQL, raw output, measured result, and engineer notes
- Fix that looks the same but silently changes the sort order: exact SQL, output, and verdict
- A manual-grounded production interpretation of the lab result
Card required. Cancel before day 7 and you are not charged.
Connected
Everything this error touches
Every page this SQLSTATE connects to: the concept that explains it, the runbooks that fix it, the parameters you tune to prevent it, and the sibling errors it travels with. All real cross-references. Jump straight in, or open the full interactive map.
Verification
- Last verified
- 2026-07-15 (Docker lab, PostgreSQL 16.14)
- Verification scope
- Verified against PostgreSQL 16.14 in an isolated lab environment
- Audit status
- reviewed
Went further?
Pro unlocks the second lab proof
Free page stops the bleeding. Pro adds the operational test, SQLSTATE audit, and deeper evidence, same error, more certainty.