WHEN OTHERS 能捕获多数运行时异常但不捕获编译错误、ORA-00600等内部错误、会话中断及资源耗尽类异常,且会丢失堆栈信息;应优先捕获预定义异常,慎用WHEN OTHERS并务必记录完整上下文后重抛。
Oracle PL/SQL 中不能用 WHEN OTHERS “捕获所有运行时异常”
这是最常被误解的一点:when others 确实能捕获绝大多数未显式声明的异常,但它**不捕获编译时错误、内部错误(如 ora-00600)、会话中断、资源耗尽(如 ora-01555)、以及部分系统级中断(如用户主动 alter system kill session)**。更重要的是,它会吞掉异常堆栈信息,导致调试困难。
健壮的异常处理不是追求“全捕获”,而是:明确知道哪些异常可预期、可恢复;哪些必须暴露;哪些需记录后重抛。
-
WHEN OTHERS应仅出现在顶层匿名块或存储过程末尾,且必须配合DBMS_UTILITY.FORMAT_ERROR_BACKTRACE和SQLERRM记录完整上下文 - 永远不要只写
NULL;或空EXCEPTION块 - 对 DML 操作,优先捕获具体异常(如
DUP_VAL_ON_INDEX、NO_DATA_FOUND),而非依赖WHEN OTHERS
必须显式声明并处理的常见预定义异常
Oracle 预定义了约 20 个常用异常,它们有名字、有编号、语义明确。用名字捕获比靠 SQLCODE 判断更安全、可读性更强。
典型场景示例:
DECLARE
l_emp_id NUMBER := 100;
l_name VARCHAR2(100);
BEGIN
SELECT ename INTO l_name FROM emp WHERE empno = l_emp_id;
EXCEPTION
WHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.PUT_LINE('员工 ' || l_emp_id || ' 不存在');
WHEN TOO_MANY_ROWS THEN
DBMS_OUTPUT.PUT_LINE('查询返回多行,但只允许一行');
WHEN INVALID_NUMBER THEN
DBMS_OUTPUT.PUT_LINE('数值转换失败:可能是字符串含非数字字符');
END;-
NO_DATA_FOUND:仅在SELECT ... INTO未找到数据时触发;UPDATE/DELETE影响 0 行不会抛此异常 -
TOO_MANY_ROWS:只针对SELECT ... INTO;游标FETCH多行是正常行为 -
INVALID_NUMBER、VALUE_ERROR常见于隐式类型转换失败,建议改用显式函数(如TO_NUMBER(... DEFAULT NULL ON CONVERSION ERROR),12c+)
自定义异常:用 PRAGMA EXCEPTION_INIT 关联 ORA 错误号
很多关键业务异常没有预定义名(比如唯一约束冲突是 ORA-00001,但名字是 DUP_VAL_ON_INDEX;而外键违例是 ORA-02291,无默认名)。这时需手动绑定。
正确做法:
DECLARE
e_fk_violated EXCEPTION;
PRAGMA EXCEPTION_INIT(e_fk_violated, -2291); -- 注意负号
l_dept_id NUMBER := 9999;
BEGIN
INSERT INTO emp (empno, ename, deptno) VALUES (9999, 'TEST', l_dept_id);
EXCEPTION
WHEN e_fk_violated THEN
RAISE_APPLICATION_ERROR(-20001, '部门 ' || l_dept_id || ' 不存在,无法插入员工');
END;- 错误号必须带负号(
-2291),否则编译报错 - 自定义异常变量必须在声明区定义,
PRAGMA必须紧随其后 - 避免绑定过于宽泛的错误号(如
-6502是VALUE_ERROR的底层码,但含义太泛,优先用预定义名)
日志 + 重抛:健壮性的核心动作
生产环境里,静默吞掉异常等于埋雷。真正健壮的处理是:记录足够信息 → 判断是否可恢复 → 不可恢复则重抛。
关键操作链:
EXCEPTION
WHEN OTHERS THEN
-- 1. 记录完整堆栈(不只是 SQLERRM)
logger.log_error(
p_module => 'PKG_EMP_MANAGE',
p_action => 'INSERT_EMP',
p_error => DBMS_UTILITY.FORMAT_ERROR_BACKTRACE || CHR(10) || SQLERRM
);
-- 2. 清理资源(如关闭游标、回滚局部事务)
IF cur_emp%ISOPEN THEN CLOSE cur_emp; END IF;
-- 3. 重抛,保留原始错误位置
RAISE;-
DBMS_UTILITY.FORMAT_ERROR_BACKTRACE返回异常发生的确切行号(包括调用栈),比SQLERRM有用得多 - 不要用
RAISE_APPLICATION_ERROR替代原异常,除非你明确要掩盖原始错误位置 - 如果做了
COMMIT或ROLLBACK,再RAISE可能违反事务一致性;此时应RAISE_APPLICATION_ERROR并附带清晰业务提示
最易被忽略的点:异常处理块本身可能出错(比如日志表空间满导致插入失败),所以关键日志语句建议加上 SAVE EXCEPTIONS 或用自治事务封装。


















