When an SQL function or procedure in PostgreSQL violates a restriction on the type of SQL statements it may execute, PostgreSQL raises an error in the 2F SQLSTATE class. The base condition name is sql_routine_exception (SQLSTATE 2F000), and you will typically see a more specific subclass code in practice, such as 2F002 (modifying_sql_data_not_permitted) or 2F005 (prohibited_sql_statement_attempted).
What This Error Means
SQLSTATE class 2F covers errors that occur inside an SQL-language routine — a function or procedure defined with LANGUAGE SQL — when that routine attempts to execute a statement that is prohibited by the routine's declared volatility or data access characteristics.
PostgreSQL SQL functions carry attributes that govern what kinds of SQL they may execute. The CONTAINS SQL, NO SQL, READS SQL DATA, and MODIFIES SQL DATA attributes (along with the more commonly used VOLATILE, STABLE, and IMMUTABLE volatility markers) define the contract between the function and the planner. When the body of a function violates that contract at runtime — for example, a function declared READS SQL DATA attempts an INSERT — PostgreSQL raises a 2F-class error.
The 2F000 code is the parent (catch-all) for this class. In practice you are more likely to encounter one of the named subconditions:
2F002—modifying_sql_data_not_permitted: the routine tried to modify table data but is not declared to do so.2F003—prohibited_sql_statement_attempted: the routine attempted a statement that is entirely prohibited in its context (for example, a transaction control statement inside a function).2F004—reading_sql_data_not_permitted: the routine tried to read table data but is declaredNO SQL.2F005—function_executed_no_return_statement: a function with a return type reached the end of its body without executing aRETURN.
The error aborts the current statement and, unless caught by an exception handler, the surrounding transaction is marked as aborted.
Common Causes
Transaction control inside a non-procedure SQL routine. Calling
COMMITorROLLBACKinside an SQL function (not aPROCEDURE) raises2F003. Transaction control is only allowed inside procedures invoked withCALL.A function declared
IMMUTABLEorSTABLEperforming writes. If a function's body containsINSERT,UPDATE, orDELETEbut the function is declaredSTABLEorIMMUTABLE, PostgreSQL may raise2F002depending on the execution context, because those volatility levels imply read-only access.A function with a declared return type missing a
RETURNstatement. If execution reaches the end of the function body without hitting aRETURN, PostgreSQL raises2F005. This is common when all code paths are insideIFbranches and one branch falls through.Calling a function declared
NO SQLthat actually queries a table. If a function is explicitly declared withNO SQL(an SQL standard attribute sometimes set via C-language function definitions or extensions), any attempt to access tables raises2F004.
How to Fix sql_routine_exception
Check the specific SQLSTATE subcode. The error message and SQLSTATE code from your logs or client will point to the exact subcondition. Use that to narrow down the cause before changing anything.
-- In psql, \errverbose after an error shows the SQLSTATE \errverboseMove transaction control to a procedure. If you need
COMMIT/ROLLBACKinside a routine, convert the function to aPROCEDUREand call it withCALL:-- Wrong: COMMIT inside a function CREATE FUNCTION process_batch() RETURNS void LANGUAGE sql AS $$ INSERT INTO log(msg) VALUES ('start'); COMMIT; -- raises 2F003 $$; -- Correct: use a procedure CREATE PROCEDURE process_batch() LANGUAGE sql AS $$ INSERT INTO log(msg) VALUES ('start'); COMMIT; $$; CALL process_batch();Align the function's volatility with its actual behavior. A function that writes data must be
VOLATILE(the default). A function that only reads data can beSTABLE. A function that accesses no tables and returns a deterministic result can beIMMUTABLE. Incorrect volatility declarations cause both correctness issues and2F-class errors:-- Correct: writing function must be VOLATILE (default, but explicit here) CREATE OR REPLACE FUNCTION archive_old_rows() RETURNS void LANGUAGE sql VOLATILE AS $$ DELETE FROM events WHERE created_at < now() - interval '90 days'; $$;Ensure every code path returns a value. For functions with a non-
voidreturn type, every branch must execute aRETURN. InLANGUAGE sql, the last statement's result is returned implicitly, but make sure no path exits the function without aSELECT/RETURN:-- Potentially missing return if no row found CREATE FUNCTION get_user_name(uid int) RETURNS text LANGUAGE sql AS $$ SELECT name FROM users WHERE id = uid; -- If no row matches, returns NULL implicitly — this is fine in SQL functions -- but in PL/pgSQL you would need an explicit RETURN $$;Catch the error in PL/pgSQL when appropriate. If you are calling a routine that may raise a
2F-class error and you want to handle it gracefully:DO $$ BEGIN PERFORM my_sql_function(); EXCEPTION WHEN sql_routine_exception THEN RAISE NOTICE 'Caught sql_routine_exception: %', SQLERRM; END; $$;
Additional Information
- The
2Ferror class is defined by the SQL standard and has been part of PostgreSQL's error code table since at least PostgreSQL 8.0. - PostgreSQL's error code reference lists all
2Fsubcodes in the appendix of the documentation under "Error Codes." - Most client drivers (psycopg2, pgx, JDBC) surface
2F-class errors as a genericDatabaseErrororProgrammingErrorwith the SQLSTATE available as a separate field — checke.pgcode(psycopg2) orgetSQLState()(JDBC) for the exact code. - ORMs that wrap functions in implicit transactions (e.g., Django with
ATOMIC_REQUESTS) can mask2F003errors; the underlying cause is still aCOMMIT/ROLLBACKinside a function. 2F005(function_executed_no_return_statement) is the most common subcondition developers encounter, usually caught at development time when testing newly written PL/pgSQL or SQL functions.
Frequently Asked Questions
Why do I see 2F000 in my logs instead of a more specific code like 2F003?
2F000 is the generic parent code for the class. You will see it when PostgreSQL raises the error without being able to assign a more specific subcode, or when your logging/monitoring tool is grouping by error class rather than the full SQLSTATE. Check the full error message text alongside the code — it usually contains a description that identifies the exact problem.
Can I use COMMIT inside a PostgreSQL function?
No. Transaction control statements (COMMIT, ROLLBACK, SAVEPOINT) are not allowed inside SQL functions or PL/pgSQL functions. They are only permitted inside stored procedures invoked via CALL. This is a deliberate design: functions are meant to execute within the caller's transaction, while procedures can manage their own transaction boundaries.
My function is declared VOLATILE but I still get a 2F error — why?
VOLATILE is a PostgreSQL-specific volatility marker, not the same as the SQL standard MODIFIES SQL DATA attribute. If you are using a C extension or a foreign data wrapper that sets function attributes using the SQL standard access category, the mismatch can still raise 2F002. Check the function definition with \df+ function_name in psql to see all attributes.
How do I find which function triggered the error?
The error message usually includes the function name and the line number within the function body. In psql you can run \errverbose immediately after the error to see the full detail, hint, and context fields, which include the call stack through nested function invocations.