PostgreSQL Error 2F000: SQL Routine Exception — Causes, Fixes & Prevention
PostgreSQL error code 2F000 (SQL Routine Exception) is a parent error class that signals something went wrong inside a SQL function or stored procedure during execution. It typically surfaces as a more specific subcode such as 2F002, 2F003, or 2F005, each pointing to a distinct violation within the routine's execution context. Understanding this error class is essential for any team running business logic inside the database layer.
Top 3 Causes
1. Transaction Control Statements Inside a Function (2F003)
Using COMMIT or ROLLBACK inside a PL/pgSQL function (not a procedure) is forbidden. Functions run within the caller's transaction and cannot manage their own transaction boundaries.
-- ❌ Wrong: COMMIT inside a function triggers 2F003
CREATE OR REPLACE FUNCTION bad_func()
RETURNS void LANGUAGE plpgsql AS $$
BEGIN
INSERT INTO orders(item) VALUES ('widget');
COMMIT; -- ERROR: 2F003 prohibited_sql_statement_attempted
END;
$$;
-- ✅ Fix: Convert to a PROCEDURE (PostgreSQL 11+)
CREATE OR REPLACE PROCEDURE good_proc()
LANGUAGE plpgsql AS $$
BEGIN
INSERT INTO orders(item) VALUES ('widget');
COMMIT; -- Allowed inside a procedure
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
RAISE NOTICE 'Rolled back due to: %', SQLERRM;
END;
$$;
CALL good_proc();
2. DML Inside a Read-Only or IMMUTABLE Function (2F002)
Declaring a function as IMMUTABLE or STABLE while performing INSERT, UPDATE, or DELETE inside it causes a 2F002 error. PostgreSQL enforces that the function's declared volatility matches its actual behavior.
-- ❌ Wrong: IMMUTABLE function attempting data modification
CREATE OR REPLACE FUNCTION bad_immutable(p_id INT)
RETURNS TEXT LANGUAGE plpgsql IMMUTABLE AS $$
DECLARE v_name TEXT;
BEGIN
-- 2F002: modifying_sql_data_not_permitted
UPDATE products SET accessed_at = NOW()
WHERE id = p_id RETURNING name INTO v_name;
RETURN v_name;
END;
$$;
-- ✅ Fix: Use VOLATILE for functions that modify data
CREATE OR REPLACE FUNCTION good_volatile(p_id INT)
RETURNS TEXT LANGUAGE plpgsql VOLATILE AS $$
DECLARE v_name TEXT;
BEGIN
UPDATE products SET accessed_at = NOW()
WHERE id = p_id RETURNING name INTO v_name;
RETURN v_name;
END;
$$;
-- ✅ IMMUTABLE is safe only for pure computation
CREATE OR REPLACE FUNCTION calc_discount(price NUMERIC, pct NUMERIC)
RETURNS NUMERIC LANGUAGE plpgsql IMMUTABLE AS $$
BEGIN
RETURN price - (price * pct / 100);
END;
$$;
3. Missing RETURN Statement in All Code Paths (2F005)
If a PL/pgSQL function can exit without hitting a RETURN statement — due to incomplete IF/ELSIF branches — PostgreSQL raises 2F005: function_executed_no_return_statement.
-- ❌ Wrong: No RETURN for scores below 70
CREATE OR REPLACE FUNCTION get_grade(score INT)
RETURNS TEXT LANGUAGE plpgsql AS $$
BEGIN
IF score >= 90 THEN RETURN 'A';
ELSIF score >= 80 THEN RETURN 'B';
ELSIF score >= 70 THEN RETURN 'C';
-- score < 70 falls through → 2F005 error!
END IF;
END;
$$;
-- ✅ Fix: Guarantee a RETURN in every branch
CREATE OR REPLACE FUNCTION get_grade(score INT)
RETURNS TEXT LANGUAGE plpgsql AS $$
BEGIN
IF score IS NULL THEN
RAISE EXCEPTION 'Score cannot be NULL'
USING ERRCODE = '22023';
END IF;
RETURN CASE
WHEN score >= 90 THEN 'A'
WHEN score >= 80 THEN 'B'
WHEN score >= 70 THEN 'C'
ELSE 'F'
END;
EXCEPTION
WHEN OTHERS THEN
RAISE LOG 'get_grade error: SQLSTATE=%, MSG=%', SQLSTATE, SQLERRM;
RAISE;
END;
$$;
Quick Fix Checklist
-
Function vs. Procedure: Need
COMMIT/ROLLBACK? UseCREATE PROCEDURE+CALL, notCREATE FUNCTION. - Volatility audit: Run the query below to spot mismatched volatility declarations:
SELECT proname, provolatile,
CASE provolatile
WHEN 'i' THEN 'IMMUTABLE'
WHEN 's' THEN 'STABLE'
WHEN 'v' THEN 'VOLATILE'
END AS volatility
FROM pg_proc
WHERE pronamespace = 'public'::regnamespace
ORDER BY proname;
-
Return path audit: Search function bodies for missing
RETURNwith\sf function_nameinpsqland trace every conditional branch manually.
Prevention Tips
1. Adopt a standard function template with exception handling.
Every function should include an EXCEPTION WHEN OTHERS block that logs SQLSTATE and SQLERRM before re-raising. This ensures no error is silently swallowed and all failures are traceable in PostgreSQL logs.
2. Write unit tests with pgTAP before deploying functions.
Use the pgTAP extension to test boundary inputs (NULL, zero, max values) for every function. Integrate these tests into your CI/CD pipeline so 2F000-class errors are caught before they reach production.
-- Quick pgTAP example
BEGIN;
SELECT plan(2);
SELECT is(get_grade(95), 'A', 'Score 95 should return A');
SELECT is(get_grade(60), 'F', 'Score 60 should return F');
SELECT * FROM finish();
ROLLBACK;
Related Error Codes
| Code | Name | Summary |
|---|---|---|
2F002 |
modifying_sql_data_not_permitted |
DML inside a read-only function |
2F003 |
prohibited_sql_statement_attempted |
Forbidden SQL (e.g., COMMIT) in a function |
2F004 |
reading_sql_data_not_permitted |
SELECT blocked in restricted context |
2F005 |
function_executed_no_return_statement |
Function exited without RETURN |
P0001 |
raise_exception |
User-defined exception via RAISE EXCEPTION |
Always consult the PostgreSQL Error Codes Appendix to distinguish between these closely related subcodes when diagnosing 2F000 in production.
📖 Want a more detailed guide?
Check out the full in-depth version (Korean) on oraerror.com — includes detailed analysis, additional SQL examples, and prevention tips.
Top comments (0)