Dev0 08 PLPGSQL Exceptions
Dev0 08 PLPGSQL Exceptions
Error Handling
17
Copyright
© Postgres Professional, 2017–2025
Authors Egor Rogov, Pavel Luzanov, Ilya Bashtanov, Igor Gnatyuk
Translated by Liudmila Mantrova and Alexander Meleshko
Photo by Oleg Bartunov (Tukuche peak, Nepal)
Feedback
Please send your feedback, comments and suggestions to:
[email protected]
Disclaimer
In no event shall Postgres Professional company be liable for any damages
or loss, including loss of profits, that arise from direct or indirect, special or
incidental use of course materials. Postgres Professional company
specifically disclaims any warranties on course materials. Course materials
are provided “as is,” and Postgres Professional company has no obligations
to provide maintenance, support, updates, enhancements, or modifications.
Topics
2
Error handling in a block
CREATE TABLE
INSERT 0 1
=> DO $$
DECLARE
n integer;
BEGIN
SELECT id INTO STRICT n FROM t;
RAISE NOTICE 'The SELECT INTO operator has completed, n = %', n;
END
$$;
INSERT 0 1
If there is no EXCEPTION section in a block, the operator execution is interrupted, and the whole block is considered to be
completed with an error:
=> DO $$
DECLARE
n integer;
BEGIN
SELECT id INTO STRICT n FROM t;
RAISE NOTICE 'The SELECT INTO operator has completed, n = %', n;
END
$$;
To catch an error, a block must have an EXCEPTION section, which defines one or more error handlers.
This construct works similar to CASE: conditions are parsed from top to bottom, the first suitable code path is selected, and its
operators are executed.
=> DO $$
DECLARE
n integer;
BEGIN
n := 3;
INSERT INTO t(id) VALUES (n);
SELECT id INTO STRICT n FROM t;
RAISE NOTICE 'The SELECT INTO operator has completed, n = %', n;
EXCEPTION
WHEN no_data_found THEN
RAISE NOTICE 'No data';
WHEN too_many_rows THEN
RAISE NOTICE 'Too much data';
RAISE NOTICE 'Rows in a table: %, n = %', (SELECT count(*) FROM t), n;
END
$$;
The executed handler corresponds to the too_many_rows error. Note: if a handler is executed, the table contains two rows because
of a rollback to an implicit savepoint at the beginning of the block.
Also note that the local variable keeps the value that was there when the error occured.
Note the following subtlety: if an error occurs in the DECLARE section or within the EXCEPTION section of the handler itself, it will
be impossible to catch it in this block.
=> DO $$
DECLARE
n integer := 1 / 0; -- an error is not trapped here
BEGIN
RAISE NOTICE 'Success';
EXCEPTION
WHEN division_by_zero THEN
RAISE NOTICE 'Division by zero';
END
$$;
Error info
error name
five-character error code
additional info: a short message, a detailed message, a hint, names of objects
related to this error
Two-level hierarchy
P0000 – plpgsql_error XX000 – internal_error
Each possible error has a name and a code (a five-character string). WHEN
clauses accept both error names and error codes.
All errors are classified into a two-level hierarchy of sorts. Each error class
has a code that ends with three zeros; it corresponds to any error with the
same first two characters in its code.
For example, the code 23000 defines the class that includes all errors
dealing with violations of integrity constraints (such as 23502, which stands
for NOT NULL constraint violation, or 23505, which indicates a UNIQUE
constraint violation).
Thus, apart from regular errors, you can specify the whole error class by its
name or code. Besides, you can use a special name OTHERS to catch any
errors (except for the fatal ones).
Apart from the name and code, each error can provide additional debug
information: a short error message, a detailed message, and a hint.
All errors are described in documentation in Appendix A:
https://postgrespro.com/docs/postgresql/17/errcodes-appendix
Errors can be not only trapped, but also raised programmatically.
https://postgrespro.com/docs/postgresql/17/plpgsql-errors-and-messages
Error names and codes
We have already seen error names; error codes are specified using SQLSTATE.
An error handler can return an error code and the corresponding message using the predefined variables SQLSTATE and SQLERRM
(the variables are undefined outside of the EXCEPTION block).
=> DO $$
DECLARE
n integer;
BEGIN
SELECT id INTO STRICT n FROM t;
EXCEPTION
WHEN SQLSTATE 'P0003' OR no_data_found THEN -- there can be several conditions
RAISE NOTICE '%: %', SQLSTATE, SQLERRM;
END
$$;
=> DO $$
DECLARE
n integer;
BEGIN
SELECT id INTO STRICT n FROM t;
EXCEPTION
WHEN no_data_found THEN
RAISE NOTICE 'No data. %: %', SQLSTATE, SQLERRM;
WHEN plpgsql_error THEN
RAISE NOTICE 'Another error. %: %', SQLSTATE, SQLERRM;
WHEN too_many_rows THEN
RAISE NOTICE 'Too much data. %: %', SQLSTATE, SQLERRM;
END
$$;
NOTICE: Another error. P0003: query returned more than one row
DO
The first applicable handler is selected, plpgsql_error in this case (remember: this is not a specific error, but an error category). We
will never get to the last error handler.
You can force an error using either its code or its name.
Here we use a special name “others,” which corresponds to any error that should be trapped (except for assertion failures and cases
when the execution is aborted by user – you can catch them separately, but you almost never need to).
=> DO $$
BEGIN
RAISE no_data_found;
EXCEPTION
WHEN others THEN
RAISE NOTICE '%: %', SQLSTATE, SQLERRM;
END
$$;
If required, it is also possible to incorporate user-provided error codes that are not predefined, as well as pass some additional
information (the example illustrates only some of the supported features):
=> DO $$
BEGIN
RAISE SQLSTATE 'ERR01' USING
message := 'Matrix failure',
detail := 'Irrecoverable matrix failure has occurred during execution',
hint := 'Contact your system administrator';
END
$$;
=> DO $$
DECLARE
message text;
detail text;
hint text;
BEGIN
RAISE SQLSTATE 'ERR01' USING
message := 'Matrix failure',
detail := 'Irrecoverable matrix failure has occurred during execution',
hint := 'Contact your system administrator';
EXCEPTION
WHEN others THEN
GET STACKED DIAGNOSTICS
message := MESSAGE_TEXT,
detail := PG_EXCEPTION_DETAIL,
hint := PG_EXCEPTION_HINT;
RAISE NOTICE E'\nmessage = %\ndetail = %\nhint = %',
message, detail, hint;
END
$$;
NOTICE:
message = Matrix failure
detail = Irrecoverable matrix failure has occurred during execution
hint = Contact your system administrator
DO
Choosing a handler
Let’s take a look at several examples of choosing a handler in nested blocks. What will be displayed?
=> DO $$
BEGIN
BEGIN
SELECT 1/0;
RAISE NOTICE 'The inner block has completed';
EXCEPTION
WHEN division_by_zero THEN
RAISE NOTICE 'Error in the inner block';
END;
RAISE NOTICE 'The outer block has completed';
EXCEPTION
WHEN division_by_zero THEN
RAISE NOTICE 'Error in the outer block';
END
$$;
An error is handled in the same block where it has occurred. The outer block is executed as if there has been no error at all.
And now?
=> DO $$
BEGIN
BEGIN
SELECT 1/0;
RAISE NOTICE 'The inner block has completed';
EXCEPTION
WHEN no_data_found THEN
RAISE NOTICE 'Error in the inner block';
END;
RAISE NOTICE 'The outer block has completed';
EXCEPTION
WHEN division_by_zero THEN
RAISE NOTICE 'Error in the outer block';
END
$$;
The handler in the inner block is not applicable; the block completes with an error that is handled in the outer block.
Remember that the block containing an EXCEPTION section is rolled back to the implicit savepoint at the beginning of this block. In
this case, all changes made in both blocks will be rolled back.
And now?
=> DO $$
BEGIN
BEGIN
SELECT 1/0;
RAISE NOTICE 'The inner block has completed';
EXCEPTION
WHEN no_data_found THEN
RAISE NOTICE 'Error in the inner block';
END;
RAISE NOTICE 'The outer block has completed';
EXCEPTION
WHEN no_data_found THEN
RAISE NOTICE 'Error in the outer block';
END
$$;
CREATE PROCEDURE
CREATE PROCEDURE
CREATE PROCEDURE
The error message displays the call stack: top to bottom means inside out.
Note that this message (like many others) uses the term “function” instead of “procedure”.
An error handler can also provide access to the call stack, but it will be presented as a single string:
CREATE PROCEDURE
CALL
Since a block with an EXCEPTION section creates an implicit savepoint, procedures cannot use COMMIT and ROLLBACK commands
both in this block and in all the blocks up the call stack.
CREATE PROCEDURE
NOTICE:
Error: invalid transaction termination
Error stack:
PL/pgSQL function baz() line 3 at COMMIT
SQL statement "CALL baz()"
PL/pgSQL function bar() line 6 at CALL
SQL statement "CALL bar()"
PL/pgSQL function foo() line 3 at CALL
CALL
Overhead
To estimate the overhead, let’s take a look at the following simple example.
Suppose we have a table with a text field that stores arbitrary data inserted by users (although usually a sign of bad design, it may
sometimes be required). We need to extract all numbers into a separate column of a numeric type.
CREATE TABLE
INSERT 0 1000000
Let’s solve this problem using error handling that comes up when converting text to integer:
CREATE FUNCTION
=> \timing on
Timing is on.
UPDATE 1000000
Time: 10031.133 ms (00:10.031)
Timing is off.
count
--------
990078
(1 row)
The following implementation of our function will check the format using a (slightly simplified) regular expression, without error
handling. The body can be written in SQL:
CREATE FUNCTION
=> \timing on
Timing is on.
UPDATE 1000000
Time: 6714.983 ms (00:06.715)
count
--------
990078
(1 row)
This implementation is significantly faster. In this example, the exception has occurred in 1% of cases only. The more often it
occurs, the more overhead will be incurred by rollbacks to the savepoint.
UPDATE 1000000
=> \timing on
Timing is on.
UPDATE 1000000
Time: 13422.992 ms (00:13.423)
Timing is off.
In some cases (which are not infrequent), you can do without error handling if you choose other suitable means.
Problem: update a table row with the specified ID; if there is no such row, insert it.
CREATE TABLE
INSERT 0 2
IF cnt = 0 THEN
INSERT INTO categories VALUES (code, description);
ELSE
UPDATE categories c
SET description = change.description
WHERE c.code = change.code;
END IF;
END
$$ VOLATILE LANGUAGE plpgsql;
CREATE FUNCTION
Almost everything is bad here, starting from the fact that such a function will not work correctly at the Read Committed isolation
level if there are several concurrent sessions. That’s because the data in the database can change between the executed SELECT
statement and the next operation.
IF cnt = 0 THEN
INSERT INTO categories VALUES (code, description);
ELSE
UPDATE categories c
SET description = change.description
WHERE c.code = change.code;
END IF;
END
$$ VOLATILE LANGUAGE plpgsql;
CREATE FUNCTION
Now let’s run this function in two different sessions, almost simultaneously:
change
--------
(1 row)
BEGIN
INSERT INTO categories VALUES (code, description);
EXIT;
EXCEPTION
WHEN unique_violation THEN NULL;
END;
END LOOP;
END
$$ VOLATILE LANGUAGE plpgsql;
CREATE FUNCTION
change
--------
(1 row)
change
--------
(1 row)
But there is an easier way: you can use a special flavor of the INSERT command that attempts to insert a row and performs an
update if a conflict occurs. Again, all you need is pure SQL.
CREATE FUNCTION
Problem: process a set of documents; a processing error of a particular document should not result in a general failure.
CREATE TYPE
CREATE TABLE
INSERT 0 100
CREATE PROCEDURE
Now let’s create a procedure that processes all documents. It loops through the documents to process them one by one and catches
an error if required.
Note that transactions are committed outside of the block that contains the EXCEPTION section.
=> CREATE PROCEDURE process_docs()
AS $$
DECLARE
doc record;
BEGIN
FOR doc IN (SELECT id FROM documents WHERE status = 'READY')
LOOP
BEGIN
CALL process_one_doc(doc.id);
UPDATE documents d
SET status = 'PROCESSED'
WHERE d.id = doc.id;
EXCEPTION
WHEN others THEN
UPDATE documents d
SET status = 'ERROR', message = sqlerrm
WHERE d.id = doc.id;
END;
COMMIT; -- there is a separate transaction for each document
END LOOP;
END
$$ LANGUAGE plpgsql;
CREATE PROCEDURE
You can set up a similar processing using a function, but then all documents will be handled within a single common transaction,
which can be a problem if processing takes a long time. This question is discussed at length in the DEV2 course.
CALL
As you can see, some of the documents have not been processed, but it has not affected the processing of the others.
It is convenient that the information about the occurred errors is stored in the table itself:
Please note once again that if an error occurs, the changes are rolled back to the savepoint at the beginning of the block; that’s why
documents with the ERROR status have not changed and still have version 1.
Takeaways
11
Practice
12
1. To determine the name of the error that has to be caught, catch all errors
(WHEN OTHERS) and display the required information (by raising another
error with the corresponding text).
Then remember to replace WITH OTHERS with a specific error: let all other
error types be handled at a higher level if there is no opportunity to do
anything useful in this particular position in the code.
(In a real environment, unique constraint violations should not be handled
either: it is better to forbid entering the same author twice at the application
level.)
Task 1. Processing duplicated author names when adding books
CREATE FUNCTION