DECLARE HANDLER 必须声明在 BEGIN 后、所有变量/游标/条件之后,且必须位于出错语句之前同一 BEGIN…END 块内;仅对后续语句生效,不回溯、不跨块、不隐式匹配。

DECLARE HANDLER 不是“写了就生效”的开关,它只对紧随其后的语句块起作用,且必须在出错语句之前、同一 BEGIN END 内声明;写错位置或类型不匹配,handler 就会静默失效。
DECLARE HANDLER 必须放在哪儿才管用
MySQL 不会回溯查找 handler,它只扫描当前 BEGIN END 块中「声明之后、执行之前」的语句。一旦你把 DECLARE CONTINUE HANDLER FOR SQLEXCEPTION 放在 INSERT 后面,或者嵌套在 IF 分支里,它就完全不起作用。
实操建议:
-
DECLARE HANDLER必须紧跟在BEGIN之后,且排在所有DECLARE变量、游标、条件之后,但在任何可执行语句(如SELECT INTO、INSERT、UPDATE)之前 - 嵌套块需各自声明 handler:外层
DECLARE HANDLER对内层BEGIN END中的错误无效 - 测试时可在 handler 下方立刻写一条必错语句,例如
INSERT INTO nonexistent_table VALUES (1);,确认是否真触发
SQLEXCEPTION、SQLWARNING 和 NOT FOUND 到底该选哪个
三者触发条件完全不同,混用等于没写。主键冲突、列不存在、除零等严重错误属于 SQLEXCEPTION(SQLSTATE 不以 '00'、'01'、'02' 开头);字符串被截断这类警告属于 SQLWARNING(SQLSTATE 以 '01' 开头);而 NOT FOUND 几乎只用于游标 FETCH 到末尾——普通 SELECT INTO 查不到数据不会触发它。
实操建议:
- 业务逻辑中需要捕获主键冲突、外键失败等,统一用
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION - 不要用
FOR 1062这类 MySQL 错误码,优先用标准 SQLSTATE,比如主键冲突是SQLSTATE '23000' - 若想区分不同错误做不同处理,可先用
GET DIAGNOSTICS提取@sqlstate,再用IF分支判断
handler 里能安全做什么
MySQL 要求 handler 体必须是原子操作:不能含事务控制、不能查表、不能嵌套声明新 handler。否则可能二次出错、死循环,甚至报 ERROR 1370 (42000): execute command denied。
实操建议:
- 只做三类事:
SET @err_flag = 1;、INSERT INTO log_table(目标表必须已存在且结构稳定)、SET v_msg = 'duplicate key'; - 别在 handler 里调用
COMMIT或ROLLBACK;需要回滚,请用DECLARE EXIT HANDLER FOR SQLEXCEPTION包住整个事务块 - 想记录完整错误信息?必须在 handler 第一条语句就执行:
GET DIAGNOSTICS CONDITION 1 @sqlstate = RETURNED_SQLSTATE, @errno = MYSQL_ERRNO, @text = MESSAGE_TEXT;,中间插个SET就失效
为什么 INSERT 报错后 handler 没拦住
最常见原因是 handler 类型和错误不匹配。比如用了 CONTINUE HANDLER FOR NOT FOUND 去捕获主键冲突,或者用了 EXIT HANDLER 却误以为它会“继续执行后续逻辑”——实际上 EXIT 会让当前 BEGIN END 块立即退出,看起来就像没捕获。
实操建议:
- 默认首选
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION,它保证 handler 执行完后继续往下走 - 避免在 handler 里再触发错误:日志表必须提前建好,字段类型要兼容,别用
SELECT ... INTO去查未知状态的表 - 低版本 MySQL(如 5.6)的
GET DIAGNOSTICS不支持多参数,只能取第一个错误;5.7+ 才稳定支持完整提取
真正难的不是写 handler,而是理解它的作用域边界和执行时序——它不像高级语言的 try/catch 那样包裹代码块,而是一个严格依赖声明顺序与块结构的静态绑定机制。稍一松懈,就变成“写了等于没写”。


















