PostgreSQL Error 39000: External Routine Invocation Exception
PostgreSQL error code 39000 (external routine invocation exception) occurs when an unhandled exception is raised inside an external language routine such as PL/Python, PL/Perl, PL/Java, or PL/R. PostgreSQL catches the external runtime's exception and wraps it into this error code before surfacing it to the client. It is one of the trickier errors to debug because the root cause lives outside PostgreSQL's native execution engine.
Top 3 Causes
1. Unhandled Exception Inside PL/Python or PL/Perl Function
The most common cause is a Python or Perl exception — such as ZeroDivisionError, TypeError, or KeyError — that is never caught inside the function body.
-- Problematic function: no exception handling
CREATE OR REPLACE FUNCTION bad_divide(a NUMERIC, b NUMERIC)
RETURNS NUMERIC
LANGUAGE plpython3u
AS $$
return a / b -- Raises ZeroDivisionError when b = 0 -> error 39000
$$;
-- Fixed function: wrap logic in try-except
CREATE OR REPLACE FUNCTION safe_divide(a NUMERIC, b NUMERIC)
RETURNS NUMERIC
LANGUAGE plpython3u
AS $$
try:
if b == 0:
plpy.error("b must not be zero")
return a / b
except Exception as e:
plpy.error(f"safe_divide failed: {str(e)}")
$$;
-- Test
SELECT safe_divide(10, 2); -- Returns 5
SELECT safe_divide(10, 0); -- Returns clear error message
2. NULL or Wrong Data Type Passed to External Routine
Passing NULL or an unexpected type to an external function that does not guard against it will trigger an AttributeError or TypeError in Python, which PostgreSQL surfaces as 39000.
-- Missing NULL check
CREATE OR REPLACE FUNCTION upper_text(val TEXT)
RETURNS TEXT
LANGUAGE plpython3u
AS $$
return val.upper() -- AttributeError if val is None -> 39000
$$;
-- Fixed: add NULL and type guards
CREATE OR REPLACE FUNCTION upper_text_safe(val TEXT)
RETURNS TEXT
LANGUAGE plpython3u
AS $$
if val is None:
return None
if not isinstance(val, str):
plpy.error(f"Expected TEXT, received {type(val).__name__}")
try:
return val.strip().upper()
except Exception as e:
plpy.error(f"upper_text_safe error: {str(e)}")
$$;
SELECT upper_text_safe('hello'); -- 'HELLO'
SELECT upper_text_safe(NULL); -- NULL (safe)
3. Missing or Incompatible External Library Import
When a PL/Python function tries to import numpy, pandas, or another third-party package that is not installed in the Python environment used by the PostgreSQL server process, an ImportError is thrown and wrapped as 39000.
-- Fails if numpy is not installed in the server's Python environment
CREATE OR REPLACE FUNCTION mean_with_numpy(vals FLOAT[])
RETURNS FLOAT
LANGUAGE plpython3u
AS $$
import numpy as np -- ImportError if numpy is missing -> 39000
return float(np.mean(vals))
$$;
-- Fixed: handle ImportError with a pure-Python fallback
CREATE OR REPLACE FUNCTION mean_safe(vals FLOAT[])
RETURNS FLOAT
LANGUAGE plpython3u
AS $$
try:
import numpy as np
return float(np.mean(vals))
except ImportError:
plpy.warning("numpy not found, using fallback")
if not vals:
return None
return sum(vals) / len(vals)
except Exception as e:
plpy.error(f"mean_safe error: {str(e)}")
$$;
SELECT mean_safe(ARRAY[1.0, 2.0, 3.0, 4.0]); -- 2.5
Quick Fix Checklist
-
Always wrap external function logic in
try-exceptand useplpy.error()to surface meaningful messages. - Validate NULL and type at the top of every function before executing any logic.
-
Match the Python environment: confirm the packages your function needs are installed in the exact Python binary that PostgreSQL uses (
python3 -c "import sys; print(sys.executable)"). -
Check PostgreSQL logs (
postgresql.log) immediately after the error — the original Python traceback is often logged there with full detail.
Prevention Tips
-
Enforce a coding standard: require all PL/Python and PL/Perl functions to include NULL guards and a top-level
try-exceptblock. Make this part of your team's code review checklist. - Add automated tests: use pgTAP or a similar framework to test edge cases (NULL input, zero division, wrong types) in CI/CD pipelines before any external routine reaches production.
-- Quick smoke test pattern
DO $$
BEGIN
-- Should return NULL, not raise 39000
IF upper_text_safe(NULL) IS NOT NULL THEN
RAISE EXCEPTION 'NULL test failed';
END IF;
RAISE NOTICE 'All smoke tests passed';
END;
$$;
Related Error Codes
| Code | Name | Notes |
|---|---|---|
| 39001 | sql_routine_exception | Exception in SQL-language routines |
| 39004 | null_value_not_allowed | NULL passed where not permitted in external routine |
| 2F005 | function_executed_no_return_statement | Function exits without a RETURN |
| 58030 | io_error | I/O failure, sometimes paired with 39000 in file-reading routines |
📖 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)