DEV Community

umzzil nng
umzzil nng

Posted on • Originally published at oraerror.com

PostgreSQL 2F000 Error: Causes and Solutions Complete Guide

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();
Enter fullscreen mode Exit fullscreen mode

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;
$$;
Enter fullscreen mode Exit fullscreen mode

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;
$$;
Enter fullscreen mode Exit fullscreen mode

Quick Fix Checklist

  • Function vs. Procedure: Need COMMIT/ROLLBACK? Use CREATE PROCEDURE + CALL, not CREATE 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;
Enter fullscreen mode Exit fullscreen mode
  • Return path audit: Search function bodies for missing RETURN with \sf function_name in psql and 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;
Enter fullscreen mode Exit fullscreen mode

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)