When a trigger function violates PostgreSQL's trigger calling convention, the server raises:
ERROR: trigger_protocol_violated
SQLSTATE: 39P01
In practice the message often includes additional context, such as:
ERROR: trigger function must return NULL or relation row type
or
ERROR: null result from a trigger body that fires before an UPDATE/INSERT event must be an explicit RETURN NULL, not just falling off the end of the function
SQLSTATE 39P01 belongs to class 39 — External Routine Invocation Exception — a class shared with PL/SQL-style invocation errors. The condition name is trigger_protocol_violated.
What This Error Means
PostgreSQL triggers are driven by a strict calling convention. A trigger function is a special kind of function that must be declared with a return type of trigger and must follow explicit rules about what it returns depending on the trigger type (row-level vs. statement-level, BEFORE vs. AFTER vs. INSTEAD OF).
For row-level BEFORE triggers, the function must return either:
- The
NEWrow (to allow the operation to proceed with that row), - A modified version of
NEW(to alter the row being inserted or updated), NULL(to silently cancel the operation for that row), or- The
OLDrow (for DELETE triggers, to allow deletion to proceed).
For row-level AFTER triggers and statement-level triggers, the return value is ignored — but PostgreSQL still requires the function to return NULL explicitly; returning a non-NULL value from an AFTER trigger is not an error in itself but returning an incompatible type is.
PostgreSQL raises 39P01 when the value returned by the trigger function is incompatible with these rules — for example, returning a plain integer, a text string, or a composite type that does not match the table's row type. The error is raised at execution time, after the trigger function has returned but before PostgreSQL applies the result.
The transaction is aborted when this error occurs. Any ongoing statement that fired the trigger is rolled back. If you are inside an explicit BEGIN block you must issue a ROLLBACK before the connection can be reused.
Common Causes
Returning the wrong type from a PL/pgSQL trigger. A function declared
RETURNS triggerreturns a regular scalar value (integer, text, boolean, etc.) instead of a row. Common mistake:RETURN TRUE;orRETURN 1;instead ofRETURN NEW;orRETURN NULL;.Falling off the end of a PL/pgSQL BEFORE trigger. In PL/pgSQL, falling off the end of a function without an explicit
RETURNyieldsNULL. For a BEFORE trigger this cancels the operation silently, which is usually unintentional and can also trigger the protocol violation error in some call paths.Returning a row from a different table or composite type. The trigger function returns a row-type variable that does not match the relation the trigger is defined on.
Trigger function written in a procedural language other than PL/pgSQL (PL/Python, PL/Perl, PL/v8, etc.) returning a Python/Perl/JavaScript value that cannot be coerced to a trigger result — for example, returning
Truefrom a PL/Python BEFORE trigger instead of returning the row dictionary orNone.Statement-level trigger returning a non-NULL row. Statement-level (
FOR EACH STATEMENT) triggers must returnNULL. ReturningNEWor any row value from a statement-level trigger violates the protocol.
How to Fix trigger_protocol_violated
For row-level BEFORE INSERT or BEFORE UPDATE triggers — return
NEW:CREATE OR REPLACE FUNCTION my_before_trigger() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN -- Modify NEW if needed NEW.updated_at := NOW(); RETURN NEW; -- Allow the operation to proceed END; $$;To cancel a row operation silently, return
NULLexplicitly:CREATE OR REPLACE FUNCTION my_conditional_trigger() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF NEW.status = 'blocked' THEN RETURN NULL; -- Cancels the INSERT/UPDATE for this row END IF; RETURN NEW; END; $$;For statement-level triggers, always return
NULL:CREATE OR REPLACE FUNCTION my_statement_trigger() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN -- Do work here RETURN NULL; -- Required for statement-level triggers END; $$;For PL/Python triggers, return the row dictionary or
None:CREATE OR REPLACE FUNCTION my_python_trigger() RETURNS trigger LANGUAGE plpython3u AS $$ if TD["new"]["status"] == "blocked": return None # Cancels the operation return "OK" # Equivalent to RETURN NEW in PL/Python $$;In PL/Python, returning the string
"OK"tells PostgreSQL to use the (possibly modified)TD["new"]row as-is; returningNonecancels a BEFORE trigger operation.Use
\df+ <function_name>in psql to inspect the function definition and confirm its return type and body:\df+ my_before_triggerConfirm the return type is
triggerand that every code path ends with a validRETURN.
Additional Information
- SQLSTATE class
39(External Routine Invocation Exception) groups errors related to how procedural language functions are called from SQL. Other codes in this class include39001(INVALID_SQLSTATE_RETURNED) and39004(NULL_VALUE_NOT_ALLOWED). - The behavior of trigger return values is documented in the PostgreSQL Trigger Functions section of the manual; the rules differ between PL/pgSQL, PL/Python, PL/Perl, and PL/Tcl.
- ORMs such as SQLAlchemy and ActiveRecord do not add any special handling for
39P01— it surfaces as a generic database error. The root cause will always be in the trigger function definition, not in the application query. - This error has existed since PostgreSQL added the external routine invocation exception class; it is not specific to any recent version. However, changes to trigger semantics between major versions (e.g., INSTEAD OF triggers added in 9.1) can expose existing trigger functions that have incorrect return paths.
Frequently Asked Questions
Why does my trigger work in development but raise 39P01 in production?
This usually means a code path that is rarely exercised in development (a specific UPDATE condition, an edge-case row value) reaches a RETURN statement that returns the wrong type or falls off the end of the function. Review all code paths — especially inside IF/CASE blocks — to ensure every path returns NEW, OLD, or NULL as appropriate.
What is the difference between returning NULL and returning NEW in a BEFORE trigger?
Returning NULL from a row-level BEFORE trigger silently suppresses the operation — the INSERT, UPDATE, or DELETE does not happen for that row, and no error is raised. Returning NEW (or a modified copy of NEW) allows the operation to proceed. Confusing these two is a common logic bug; 39P01 itself occurs only when you return an incompatible type, not when you return NULL intentionally.
Can I get this error from a trigger written by a third-party extension? Yes. Extensions that install trigger functions (e.g., audit logging extensions, row-level security helpers) can contain bugs that violate the trigger protocol. After an extension upgrade, check the release notes for trigger function changes, and test against your PostgreSQL version.
How do I find which trigger is causing the error?
The full error detail in PostgreSQL logs (with log_min_messages = DEBUG1 or by examining the CONTEXT line in the error) will name the trigger and the relation. You can also query pg_trigger to list all triggers on a table:
SELECT tgname, tgtype, proname
FROM pg_trigger
JOIN pg_proc ON pg_trigger.tgfoid = pg_proc.oid
WHERE tgrelid = 'your_table'::regclass;