ORA-06512: What It Means, Why It Appears, and How to Fix It
If you've spent more than a week writing PL/SQL, you've seen a wall of red text that includes ORA-06512 repeated two or three times. Most developers panic, Google the error code, and end up confused because every answer says something different. That confusion happens because people treat ORA-06512 as the problem when it's actually the map to the problem — and there's a huge difference between the two.
Oracle's PL/SQL engine executes code in layers: anonymous blocks call procedures, procedures call functions, functions raise exceptions. When something goes wrong deep in that stack, Oracle doesn't just hand you the line that exploded — it hands you every layer the error passed through on the way back to you. ORA-06512 is Oracle's way of printing that call stack, one frame at a time, so you can trace the error back to its exact origin.
By the end of this article you'll be able to read an ORA-06512 stack trace like a senior DBA — identifying the root error, the exact line number that caused it, every procedure it bubbled through, and which exception-handling pattern would have caught it cleanly. You'll also know the three most common mistakes developers make when they see this error and exactly how to stop making them.
Reading the ORA-06512 Stack Trace Like a Pro
The golden rule: ORA-06512 is never the first error in the message block. Always scroll up. The actual error — ORA-01403, ORA-01400, ORA-20001, whatever it is — sits above the ORA-06512 lines. Those lines beneath it are the call stack, printed in reverse order from innermost to outermost.
Each ORA-06512 line follows this format: ORA-06512: at "SCHEMA.OBJECT_NAME", line N. That tells you the schema, the named object (procedure, function, package), and the exact line number inside that object where execution was happening when the error propagated through it. The very first ORA-06512 line after the root error is the precise location where the exception was originally raised or left unhandled.
Think of it like a stack of plates. The bottom plate is where the error started. Each plate above it is a caller that received the error and didn't catch it. Oracle prints the stack from bottom to top — innermost first, outermost last. Once you internalize that reading order, the message goes from intimidating wall of red to a precise debugging roadmap.
The line numbers Oracle reports are relative to the beginning of the stored object's source code as Oracle compiled it, not relative to any editor or file. If your package has a header section, count from line 1 of the entire package body when you're hunting the offending line.
-- ============================================================ -- DEMO: Producing and reading a real ORA-06512 stack trace -- Run each block in sequence in SQL*Plus or SQL Developer -- ============================================================ -- Step 1: Create the innermost procedure — this is where the -- real error will actually be raised. CREATE OR REPLACE PROCEDURE validate_employee_salary( p_employee_id IN employees.employee_id%TYPE, p_new_salary IN employees.salary%TYPE ) AS v_current_salary employees.salary%TYPE; BEGIN -- Fetch the existing salary; raises NO_DATA_FOUND if employee missing SELECT salary INTO v_current_salary -- Line 13 relative to procedure start FROM employees WHERE employee_id = p_employee_id; -- Business rule: new salary cannot be less than current salary IF p_new_salary < v_current_salary THEN -- Raise a custom application error — this becomes the ROOT error RAISE_APPLICATION_ERROR( -20001, -- Line 20: THIS is where the bomb drops 'New salary ' || p_new_salary || ' is below current salary ' || v_current_salary || ' for employee ' || p_employee_id ); END IF; END validate_employee_salary; / -- Step 2: Create a middle-layer procedure that calls the inner one -- but does NOT handle exceptions — so errors bubble upward CREATE OR REPLACE PROCEDURE process_salary_update( p_employee_id IN employees.employee_id%TYPE, p_new_salary IN employees.salary%TYPE ) AS BEGIN -- Calls inner procedure — no exception handler here, so any -- error from validate_employee_salary will propagate upward validate_employee_salary( -- Line 9 of this procedure p_employee_id => p_employee_id, p_new_salary => p_new_salary ); -- If we get here, the salary was valid — apply the update UPDATE employees SET salary = p_new_salary WHERE employee_id = p_employee_id; COMMIT; END process_salary_update; / -- Step 3: Anonymous block that calls the middle layer -- Deliberately triggers the salary-drop scenario BEGIN process_salary_update( p_employee_id => 100, -- Assume employee 100 earns 24000 p_new_salary => 5000 -- This is lower — will trigger ORA-20001 ); END; /
ORA-06512: at "HR.VALIDATE_EMPLOYEE_SALARY", line 20
ORA-06512: at "HR.PROCESS_SALARY_UPDATE", line 9
ORA-06512: at line 2
-- HOW TO READ THIS:
-- Line 1 → The ROOT cause: our custom business-rule error
-- Line 2 → Innermost named object: validate_employee_salary, line 20
-- (the RAISE_APPLICATION_ERROR call)
-- Line 3 → Middle layer: process_salary_update, line 9
-- (the call to validate_employee_salary)
-- Line 4 → The anonymous block caller, line 2
-- (the call to process_salary_update)
Why ORA-06512 Appears and the Three Exception-Handling Patterns That Prevent It Bubbling
ORA-06512 appears when an exception is raised inside a PL/SQL block and there is no EXCEPTION handler in that block to catch it. Oracle has no choice but to propagate the error upward to the caller, and as it does, it stamps each frame with an ORA-06512 line to record that the error passed through there.
There are three patterns for dealing with this, and choosing the wrong one is the most common source of debugging grief in PL/SQL codebases.
Pattern 1 — Catch and re-raise with context: You catch the exception, add useful diagnostic information (like which employee ID caused the problem), then re-raise. This preserves the full stack trace AND adds context that the original error couldn't include.
Pattern 2 — Catch and log silently: Useful for background jobs or batch processes where you don't want a single bad row to kill everything. You log the error to an error table and continue. The risk here is silent data loss if you're not disciplined about logging.
Pattern 3 — Let it propagate intentionally: Sometimes you want the caller to decide what to do with an error. This is the right choice for utility procedures. But document it explicitly so the next developer doesn't think it's an oversight.
The worst thing you can do is a bare WHEN OTHERS THEN NULL — that swallows every exception silently and makes bugs disappear without being fixed, which is a production support nightmare.
-- ============================================================ -- Pattern 1: Catch, enrich with context, and re-raise -- This gives the caller a MORE useful error than the raw one -- ============================================================ CREATE OR REPLACE PROCEDURE apply_annual_bonus( p_department_id IN departments.department_id%TYPE, p_bonus_percent IN NUMBER ) AS v_employee_id employees.employee_id%TYPE; v_current_salary employees.salary%TYPE; -- Cursor to process every employee in the department CURSOR c_dept_employees IS SELECT employee_id, salary FROM employees WHERE department_id = p_department_id; BEGIN FOR r_employee IN c_dept_employees LOOP v_employee_id := r_employee.employee_id; v_current_salary := r_employee.salary; UPDATE employees SET salary = salary * (1 + p_bonus_percent / 100) WHERE employee_id = v_employee_id; END LOOP; COMMIT; EXCEPTION -- Pattern 1: Catch anything, add rich context, re-raise -- SQLERRM gives the original error message -- SQLCODE gives the original error number WHEN OTHERS THEN -- Roll back any partial changes from this call ROLLBACK; -- Re-raise with a new message that includes what we were doing -- RAISE_APPLICATION_ERROR with the original SQLERRM embedded RAISE_APPLICATION_ERROR( -20002, 'apply_annual_bonus failed for department ' || p_department_id || ' while processing employee ' || NVL(TO_CHAR(v_employee_id), 'UNKNOWN') || '. Original error: ' || SQLERRM -- Embeds the root error text ); END apply_annual_bonus; / -- ============================================================ -- Pattern 2: Catch, log to error table, continue (batch-safe) -- ============================================================ CREATE OR REPLACE PROCEDURE batch_validate_all_salaries AS v_error_count NUMBER := 0; BEGIN FOR r_emp IN (SELECT employee_id, salary FROM employees) LOOP BEGIN -- Inner BEGIN..END creates a sub-block with its own handler -- so one failure doesn't abort the entire loop validate_employee_salary( p_employee_id => r_emp.employee_id, p_new_salary => r_emp.salary ); EXCEPTION WHEN OTHERS THEN v_error_count := v_error_count + 1; -- Log to an error tracking table instead of raising INSERT INTO salary_validation_errors ( employee_id, error_code, error_message, error_timestamp ) VALUES ( r_emp.employee_id, SQLCODE, -- Numeric error code, e.g. -20001 SQLERRM, -- Full error message text SYSTIMESTAMP ); END; -- End of sub-block — loop continues to next employee END LOOP; COMMIT; DBMS_OUTPUT.PUT_LINE( 'Batch complete. Errors encountered: ' || v_error_count ); END batch_validate_all_salaries; /
ORA-20002: apply_annual_bonus failed for department 50 while processing
employee 124. Original error: ORA-02290: check constraint violated
-- Pattern 2 output after processing all employees:
Batch complete. Errors encountered: 3
-- (3 rows written to salary_validation_errors table)
Using DBMS_UTILITY.FORMAT_ERROR_BACKTRACE to Get the Exact Line Number
Here's a frustrating gotcha: once you catch an exception in an EXCEPTION block and then call SQLERRM, Oracle only gives you the error message — not the exact line number inside the current procedure where it originated. The ORA-06512 stack you saw before the handler caught it is gone.
This is where DBMS_UTILITY.FORMAT_ERROR_BACKTRACE becomes essential. Call it inside your EXCEPTION handler, immediately after the exception is caught, and it returns a string containing the full ORA-06512-style stack trace pointing directly to the line that raised the exception — even if multiple nested calls were involved.
The critical word there is 'immediately.' Every PL/SQL statement you execute after catching the exception can overwrite the backtrace. If you call another procedure before saving the backtrace, you'll lose the original location data. The pattern is: catch the exception, call FORMAT_ERROR_BACKTRACE as the very first thing, store it, then do whatever else you need.
Paired with DBMS_UTILITY.FORMAT_ERROR_STACK (which gives you the full error stack including all ORA- codes) and DBMS_UTILITY.FORMAT_CALL_STACK (which gives you the current live call stack even before an error), these three functions give you complete observability into what your PL/SQL code is doing at any moment.
-- ============================================================ -- Production-grade error logging using FORMAT_ERROR_BACKTRACE -- This is the pattern used in real enterprise Oracle systems -- ============================================================ -- First, a table to hold our structured error logs CREATE TABLE plsql_error_log ( log_id NUMBER GENERATED ALWAYS AS IDENTITY, procedure_name VARCHAR2(128), error_code NUMBER, error_message VARCHAR2(4000), error_backtrace VARCHAR2(4000), -- The ORA-06512 trail error_stack VARCHAR2(4000), -- Full ORA- error stack call_stack VARCHAR2(4000), -- Who called whom logged_at TIMESTAMP DEFAULT SYSTIMESTAMP, session_user VARCHAR2(128) DEFAULT SYS_CONTEXT('USERENV','SESSION_USER') ); / -- A reusable logging procedure — call this from any exception handler CREATE OR REPLACE PROCEDURE log_plsql_error( p_procedure_name IN VARCHAR2, p_backtrace IN VARCHAR2, -- Pass in pre-captured backtrace p_error_stack IN VARCHAR2 -- Pass in pre-captured error stack ) AS PRAGMA AUTONOMOUS_TRANSACTION; -- Log even if caller rolls back BEGIN INSERT INTO plsql_error_log ( procedure_name, error_code, error_message, error_backtrace, error_stack ) VALUES ( p_procedure_name, SQLCODE, SQLERRM, p_backtrace, p_error_stack ); COMMIT; -- Autonomous transaction commits independently END log_plsql_error; / -- The main procedure showing the capture pattern CREATE OR REPLACE PROCEDURE recalculate_department_budget( p_department_id IN departments.department_id%TYPE ) AS v_total_salary NUMBER; v_headcount NUMBER; v_backtrace VARCHAR2(4000); -- Will hold our breadcrumb trail v_error_stack VARCHAR2(4000); -- Will hold the full ORA- stack BEGIN SELECT SUM(salary), COUNT(*) INTO v_total_salary, v_headcount FROM employees WHERE department_id = p_department_id; -- Deliberately cause a divide-by-zero if dept has no employees -- This raises ORA-01476: divisor is equal to zero UPDATE departments SET budget_per_head = v_total_salary / v_headcount -- Line 44 WHERE department_id = p_department_id; COMMIT; EXCEPTION WHEN OTHERS THEN -- CAPTURE THE BACKTRACE FIRST — before any other statement -- This preserves the exact line 44 reference in the backtrace v_backtrace := DBMS_UTILITY.FORMAT_ERROR_BACKTRACE; v_error_stack := DBMS_UTILITY.FORMAT_ERROR_STACK; -- Now safe to do other work — backtrace is saved in variables ROLLBACK; -- Log using autonomous transaction so the rollback above -- does not wipe out our error log entry log_plsql_error( p_procedure_name => 'RECALCULATE_DEPARTMENT_BUDGET', p_backtrace => v_backtrace, p_error_stack => v_error_stack ); -- Re-raise so the caller knows something went wrong RAISE; END recalculate_department_budget; / -- Test it: BEGIN recalculate_department_budget(p_department_id => 999); -- No employees END; / -- Check what was logged: SELECT procedure_name, error_code, error_message, error_backtrace FROM plsql_error_log ORDER BY logged_at DESC FETCH FIRST 1 ROWS ONLY;
ORA-01476: divisor is equal to zero
ORA-06512: at "HR.RECALCULATE_DEPARTMENT_BUDGET", line 44
ORA-06512: at line 2
-- What the plsql_error_log table contains:
PROCEDURE_NAME ERROR_CODE ERROR_MESSAGE
------------------------------ ----------- ------------------------------------
RECALCULATE_DEPARTMENT_BUDGET -1476 ORA-01476: divisor is equal to zero
ERROR_BACKTRACE
-----------------------------------------------------------------
ORA-06512: at "HR.RECALCULATE_DEPARTMENT_BUDGET", line 44
-- Line 44 is exactly where `v_total_salary / v_headcount` lives.
-- Even though the handler caught and re-raised, FORMAT_ERROR_BACKTRACE
-- preserved the original origin point.
The ORA-06512 Patterns You'll See in Production and What Each One Means
Not all ORA-06512 stacks look the same, and experienced Oracle developers can diagnose the rough shape of a problem just by looking at the structure of the error block before reading a single line of code.
A short stack (one or two ORA-06512 lines) usually means the error was raised close to where it was called — typically a direct RAISE_APPLICATION_ERROR in a procedure called from an anonymous block. These are usually the easiest to fix.
A long stack (five or more ORA-06512 lines) is a signal that your architecture has deep call chains without exception handling at intermediate layers. The error is bubbling all the way from a low-level utility up through a business layer, a service layer, and then a top-level procedure. This isn't inherently wrong, but it does mean your debugging starting point is far from your call entry point.
A stack that includes package names with dot notation — like HR.EMPLOYEE_PKG.VALIDATE_SALARY — tells you the error passed through a packaged procedure. The line number in that frame refers to the line within the package body, which you'll need to open to investigate.
An ORA-06512 with no object name, just at line N, means the error reached an anonymous block or a top-level trigger. Anonymous blocks don't have names so Oracle can't print one — it just tells you the line within that block.
-- ============================================================ -- Pattern: Trigger-originated ORA-06512 -- Triggers are a common source of confusing stack traces because -- they fire invisibly during DML — the developer doing the INSERT -- doesn't always know a trigger exists -- ============================================================ -- A BEFORE INSERT trigger that enforces a business rule CREATE OR REPLACE TRIGGER enforce_salary_band BEFORE INSERT OR UPDATE OF salary ON employees FOR EACH ROW DECLARE v_min_salary job_grades.min_salary%TYPE; v_max_salary job_grades.max_salary%TYPE; BEGIN -- Look up the allowed salary range for this job SELECT min_salary, max_salary INTO v_min_salary, v_max_salary FROM job_grades WHERE job_id = :NEW.job_id; -- :NEW is the incoming row -- If the new salary falls outside the band, reject the DML IF :NEW.salary NOT BETWEEN v_min_salary AND v_max_salary THEN RAISE_APPLICATION_ERROR( -- Line 21 of trigger -20010, 'Salary ' || :NEW.salary || ' is outside the band [' || v_min_salary || ' - ' || v_max_salary || '] for job ' || :NEW.job_id ); END IF; END enforce_salary_band; / -- Now try to insert an out-of-band salary INSERT INTO employees ( employee_id, first_name, last_name, email, job_id, salary, department_id ) VALUES ( 207, 'Alex', 'Chen', 'ACHEN', 'IT_PROG', 999999, -- Way above any IT_PROG band 60 ); -- ============================================================ -- Pattern: Package body stack trace -- Shows how dot-notation reveals package membership -- ============================================================ CREATE OR REPLACE PACKAGE BODY employee_pkg AS PROCEDURE hire_employee( p_first_name IN VARCHAR2, p_last_name IN VARCHAR2, p_job_id IN VARCHAR2, p_salary IN NUMBER, p_dept_id IN NUMBER ) AS v_new_emp_id NUMBER; BEGIN SELECT employee_seq.NEXTVAL INTO v_new_emp_id FROM dual; INSERT INTO employees ( employee_id, first_name, last_name, email, job_id, salary, department_id ) VALUES ( v_new_emp_id, p_first_name, p_last_name, UPPER(SUBSTR(p_first_name,1,1) || p_last_name), p_job_id, p_salary, p_dept_id ); -- The trigger fires HERE COMMIT; END hire_employee; END employee_pkg; / -- Calling the package procedure — trigger fires mid-INSERT BEGIN employee_pkg.hire_employee( p_first_name => 'Jordan', p_last_name => 'Rivera', p_job_id => 'IT_PROG', p_salary => 999999, -- Still out of band p_dept_id => 60 ); END; /
ORA-20010: Salary 999999 is outside the band [4000 - 10000] for job IT_PROG
ORA-06512: at "HR.ENFORCE_SALARY_BAND", line 21
ORA-04088: error during execution of trigger 'HR.ENFORCE_SALARY_BAND'
-- Error from package procedure call:
ORA-20010: Salary 999999 is outside the band [4000 - 10000] for job IT_PROG
ORA-06512: at "HR.ENFORCE_SALARY_BAND", line 21
ORA-04088: error during execution of trigger 'HR.ENFORCE_SALARY_BAND'
ORA-06512: at "HR.EMPLOYEE_PKG", line 17
ORA-06512: at line 2
-- NOTICE: ORA-04088 tells you a TRIGGER was involved.
-- The stack reads: trigger (line 21) → package body (line 17) → anonymous block
-- Whenever you see ORA-04088 in a stack, a trigger is the source.
| Approach | Catches Root Error | Preserves Line Numbers | Works After ROLLBACK | Best Used For |
|---|---|---|---|---|
| SQLERRM in EXCEPTION block | Yes — error message text | No — line info is lost once caught | Yes | Simple error messages in small procedures |
| DBMS_UTILITY.FORMAT_ERROR_BACKTRACE | Partial — stack only, not message | Yes — exact line of origin preserved | Yes if captured before ROLLBACK | Production logging, enterprise systems |
| DBMS_UTILITY.FORMAT_ERROR_STACK | Yes — full ORA- chain | No — message text only, no lines | Yes if captured before ROLLBACK | Logging the full error code chain |
| Let error propagate (no handler) | Yes — full stack visible to caller | Yes — Oracle prints full ORA-06512 trail | N/A — no explicit rollback control | Utility procedures where caller decides |
| WHEN OTHERS THEN NULL | No — silently swallowed | No — completely lost | Yes (nothing to preserve) | Never — this is an anti-pattern |
🎯 Key Takeaways
- ORA-06512 is a call stack reporter, not the actual error — the real error is always the ORA- code printed above it, and that is what you fix.
- DBMS_UTILITY.FORMAT_ERROR_BACKTRACE must be the very first call in your EXCEPTION handler — every statement you execute before it risks overwriting the original line number.
- PRAGMA AUTONOMOUS_TRANSACTION is non-negotiable in any error-logging procedure — without it, a ROLLBACK in the calling block silently erases your log entry.
- When ORA-04088 appears in an ORA-06512 stack, a database trigger is in the chain — query ALL_TRIGGERS to find it, because the developer running the DML may not know the trigger exists.
⚠ Common Mistakes to Avoid
- ✕Mistake 1: Trying to fix ORA-06512 directly — Symptom: Developer searches for 'ORA-06512 fix' and doesn't find the root cause — Fix: Always find and fix the error listed ABOVE the ORA-06512 lines. ORA-06512 is a location reporter, not an error itself. The actual error code (e.g., ORA-01403, ORA-20001) is always the first line of the block.
- ✕Mistake 2: Calling FORMAT_ERROR_BACKTRACE too late — Symptom: The backtrace returns a different line number or NULL because another PL/SQL call executed first inside the EXCEPTION block — Fix: Assign DBMS_UTILITY.FORMAT_ERROR_BACKTRACE to a local variable as the very first executable statement inside the WHEN OTHERS THEN handler, before any INSERT, procedure call, or other statement.
- ✕Mistake 3: Missing PRAGMA AUTONOMOUS_TRANSACTION in the error logging procedure — Symptom: The error gets logged to the table, then the caller does ROLLBACK, and the log entry disappears — leaving you with no record of what went wrong — Fix: Add PRAGMA AUTONOMOUS_TRANSACTION to the logging procedure's declaration section. This makes it commit in its own independent database session, immune to the caller's ROLLBACK.
Interview Questions on This Topic
- QORA-06512 keeps appearing in your logs. Walk me through exactly how you would identify the root cause and the line of code responsible — what tools and functions would you use?
- QWhat is the difference between SQLERRM and DBMS_UTILITY.FORMAT_ERROR_BACKTRACE, and why might SQLERRM fail to give you the line number of the original error?
- QYou have a batch PL/SQL procedure that processes 10,000 rows. One row fails mid-batch. How would you design the exception handling so the failure is logged but the remaining rows continue processing — and what happens to the error log if the entire transaction rolls back?
Frequently Asked Questions
What does ORA-06512 actually mean in Oracle PL/SQL?
ORA-06512 means 'at object_name, line N' — it's Oracle printing one frame of the PL/SQL call stack to show you the path an unhandled exception took as it bubbled up through your code. It is not an error in itself; it's a location breadcrumb. The actual error is always the ORA- code printed on the line above the first ORA-06512 entry.
Does fixing the code at the ORA-06512 line number fix the error?
Not directly — the ORA-06512 line number shows where execution was when the error passed through that frame, but the fix should target the root error printed above the ORA-06512 block. For example, if the root error is ORA-01403 (no data found), you fix the SELECT statement or add an exception handler for NO_DATA_FOUND, not the line Oracle tagged with ORA-06512.
How do I find the exact line number of an Oracle PL/SQL error after catching it in an exception handler?
Use DBMS_UTILITY.FORMAT_ERROR_BACKTRACE immediately as the first statement inside your EXCEPTION block and store the result in a VARCHAR2 variable. This function returns the ORA-06512 style stack pointing to the exact line where the exception originated, even after it has been caught. If you call any other procedure or run any SQL before capturing the backtrace, Oracle may overwrite the internal backtrace buffer.
If I have WHEN OTHERS THEN and I call RAISE to re-raise the exception, does the ORA-06512 stack change?
Yes, and this is a subtle trap. A bare RAISE inside an exception handler re-raises the original exception with its original error code and message, but Oracle resets the backtrace origin to the line of the RAISE statement itself — not the original line where the exception was first raised. To preserve the original backtrace for logging, always capture DBMS_UTILITY.FORMAT_ERROR_BACKTRACE before the RAISE, not after. The outer caller will see an updated ORA-06512 stack pointing to your RAISE line, but your log table will have the true origin.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.