11.7.3.3 编写处理错误的代码

发出错误的通常效果是终止正在运行的命令,并立即返回到 Emacs 编辑器的命令循环。你可以通过使用特殊形式 condition-case 建立错误处理程序,来捕获程序某一部分中发生的错误。 一个简单的示例如下:

(condition-case nil
    (delete-file filename)
  (error nil))

这段代码会删除名为 filename 的文件,并捕获所有可能出现的错误;若发生错误,则返回 nil。(对于这类简单场景,你可以使用宏 ignore-errors 来实现;详见下文。)

condition-case 结构常被用于捕获可预见的错误,例如调用 insert-file-contents 时打开文件失败的情况。它也可用于捕获完全不可预见的错误,比如程序对从用户处读取的表达式进行求值时产生的错误。

condition-case 的第二个参数被称为受保护形式(protected form)。(在上述示例中,受保护形式是对 delete-file 的调用。)当该形式开始执行时,错误处理程序即刻生效;当该形式执行完毕返回时,错误处理程序便会失效。在这期间的所有时刻,错误处理程序均保持生效状态。具体来说,在该形式调用的函数、这些函数的子例程等的执行过程中,错误处理程序都处于生效状态。这是一个很实用的特性,因为严格来讲,错误只能由受保护形式调用的 Lisp 基本函数(包括 signalerror)触发,而非受保护形式本身。

受保护形式之后的参数均为处理程序。每个处理程序会列出一个或多个 条件名(condition names)(即符号),用于指定它能处理的错误类型。触发错误时指定的错误符号也会定义一个条件名列表。只要处理程序与错误有任一共同的条件名,该处理程序就适用于此错误。在上述示例中,仅有一个处理程序,它指定了一个条件名 error,该条件名可覆盖所有类型的错误。

查找可用处理程序时,会从最近建立的处理程序开始,检查所有已建立的处理程序。因此,若两个嵌套的 condition-case 结构都声明要处理同一错误,则内层的那个会优先处理该错误。

若某个错误被某一 condition-case 结构处理,通常会阻止调试器运行 —— 即便 debug-on-error 配置要求该错误触发调试器也是如此。

若你希望能够调试被 condition-case 捕获的错误,可将变量 debug-on-signal 设置为非 nil 值。你也可以为特定处理程序指定先运行调试器,只需在条件列表中写入 debug 即可,示例如下:

(condition-case nil
    (delete-file filename)
  ((debug error) nil))

此处 debug 的作用仅为阻止 condition-case 抑制对调试器的调用。任何给定的错误是否会触发调试器,仍取决于 debug-on-error 及其他常规过滤机制的设置。See 发生错误时进入调试器

Macro: condition-case-unless-debug var protected-form handlers…

condition-case-unless-debug 提供了另一种对这类表达式进行调试的方式。它的行为与 condition-case 完全一致,除非变量 debug-on-error 不为 nil,此时它将完全不处理任何错误。

一旦 Emacs 判定某个处理函数负责处理该错误,就会将控制权转交给该处理函数。为此,Emacs 会解除所有在退出过程中由绑定结构创建的变量绑定,并执行所有正在退出的 unwind-protect 表达式的清理操作。当控制权到达处理函数时,处理函数的主体将正常执行。

处理函数主体执行完毕后,执行流程从 condition-case 表达式返回。由于受保护表达式在处理函数执行前已完全退出,处理函数无法在错误发生点恢复执行,也无法访问受保护表达式内部建立的变量绑定。它只能完成清理工作并继续执行。

错误的抛出与处理和 throwcatch 有几分相似(see 显式非局部退出:catchthrow),但它们是完全独立的两套机制。错误无法被 catch 捕获,而 throw 也无法被错误处理函数处理(尽管在没有匹配 catch 的情况下执行 throw 会抛出一个可以被处理的错误)。

Special Form: condition-case var protected-form handlers…

这个特殊形式会在 protected-form 执行期间,建立起错误处理函数 handlers。如果 protected-form 执行时没有发生错误,它的返回值就会成为 condition-case 形式的值(在没有成功处理函数的情况下;见下文)。在这种情况下,condition-case 不会产生任何效果。只有当 protected-form 执行过程中发生错误时,condition-case 才会发挥作用。

每个 handlers 都是形如 (conditions body…) 的列表。其中 conditions 是要处理的错误条件名,或是一个条件名列表(可以包含 debug,以便在处理函数执行前先运行调试器)。条件名 t 可以匹配任意条件。body 是当该处理函数捕获到错误时要执行的一条或多条 Lisp 表达式。下面是处理函数的示例:

(error nil)

(arith-error (message "Division by zero"))

((arith-error file-error)
 (message
  "Either division by zero or failure to open a file"))

发生的每个错误都带有一个 错误符号(error symbol),用于描述错误类型,同时也对应一组条件名称列表(see 错误符号与条件名称)。Emacs 会在所有生效的 condition-case 形式中,查找指定了这些条件名称之一的处理函数;最内层匹配的 condition-case 负责处理该错误。在这个 condition-case 内部,第一个匹配的处理函数处理该错误。

执行完处理函数的主体后,condition-case 会正常返回,并以处理函数主体中最后一个表达式的值作为整体返回值。

参数 var 是一个变量。condition-case 在执行 protected-form 时不会绑定该变量,只有在处理错误时才会绑定。此时,它会将 var 局部绑定到一个 错误描述(error description) 上,这是一个包含错误具体信息的列表。错误描述的格式为 (error-symbol . data)。处理函数可以引用这个列表来决定后续操作。例如,如果错误是打开文件失败,那么文件名就是 data 的第二个元素 — 也就是错误描述的第三个元素。

如果 varnil,则表示不绑定任何变量。此时处理函数无法获取错误符号及其相关数据。

作为一种特殊情况,handlers 中可以包含一个格式为 (:success body…) 的处理项。当 protected-form 无错误正常结束时,会执行这里的 body,并且(如果 var 非空)会把 var 绑定到 protected-form 的返回值上。

有时需要将被 condition-case 捕获的信号重新抛出,以便让更外层的处理函数捕获。做法如下:

  (signal (car err) (cdr err))

其中 err 是错误描述变量,即 condition-case 的第一个参数,你希望重新抛出该参数对应的错误条件。 See Definition of signal

Function: error-message-string error-descriptor

该函数返回给定错误描述符对应的错误消息字符串。如果你希望通过打印该错误对应的常规错误消息来处理错误,这个函数会非常有用。See Definition of signal

以下是一个使用 condition-case 处理除以零导致的错误的示例。该处理函数会显示错误消息(但不发出提示音),然后返回一个极大的数值。

(defun safe-divide (dividend divisor)
  (condition-case err
      ;; Protected form.
      (/ dividend divisor)
    ;; The handler.
    (arith-error                        ; Condition.
     ;; Display the usual message for this error.
     (message "%s" (error-message-string err))
     1000000)))
⇒ safe-divide

(safe-divide 5 0)
     ⊣ Arithmetic error: (arith-error)
⇒ 1000000

该处理函数指定了条件名 arith-error,因此它只处理除以零的错误。其他类型的错误不会被(这个 condition-case)捕获处理。因此:

(safe-divide nil 3)
     error→ Wrong type argument: number-or-marker-p, nil

这里是一个能够捕获所有类型错误(包括来自 error 的错误)的 condition-case 示例:

(setq baz 34)
     ⇒ 34

(condition-case err
    (if (eq baz 35)
        t
      ;; This is a call to the function error.
      (error "Rats!  The variable %s was %s, not 35" 'baz baz))
  ;; This is the handler; it is not a form.
  (error (princ (format "The error was: %s" err))
         2))
⊣ The error was: (error "Rats!  The variable baz was 34, not 35")
⇒ 2
Macro: ignore-errors body…

该结构执行 body,并忽略其执行过程中发生的所有错误。如果执行过程中未出现错误,ignore-errors 返回 body 中最后一个表达式的值;若发生错误,则返回 nil

以下是将本节开头的示例改用 ignore-errors 后的写法:

  (ignore-error end-of-file
    (read ""))

condition 也可以是一个错误条件列表。

Macro: with-demoted-errors format body…

该宏类似于 ignore-errors 的「温和版」。它不会完全抑制错误,而是将错误转换为提示消息。该宏会使用字符串 format 来格式化这条消息。format 中应包含且仅包含一个 ‘%’ 格式序列(例如 "Error: % S")。在那些不预期会抛出错误、但出错时需保证鲁棒性的代码外层,可使用 with-demoted-errors。注意,该宏使用的是 condition-case-unless-debug,而非 condition-case

有时我们希望捕获部分错误,并记录错误发生时的上下文信息 —— 例如完整的调用栈回溯(backtrace)、当前缓冲区(buffer)等。遗憾的是,这类信息无法在 condition-case 的处理函数中获取:因为在运行处理函数之前,调用栈已被展开(unwound),所以处理函数的执行上下文是 condition-case 所在的动态环境,而非错误抛出的位置。针对这类场景,你可以使用以下形式:

Macro: handler-bind handlers body…

该特殊形式执行 body;若 body 执行过程中未发生错误,其返回值将作为 handler-bind 形式的返回值。这种情况下,handler-bind 不会产生任何作用。

handlers 应为一个列表,其元素格式为 (conditions handler):其中 conditions 是待处理的错误条件名(或条件名列表),handler 则是一个经求值后应返回函数的表达式。与 condition-case 一致,条件名均为符号类型。

在执行 body 之前,handler-bind 会对所有 handler 表达式求值,并将这些处理函数注册为「在 body 求值期间生效」的处理逻辑。当错误被抛出时,Emacs 会遍历所有生效的 condition-casehandler-bind 形式,查找指定了该错误对应条件名的处理函数;若最内层匹配的处理函数是由 handler-bind 注册的,则会调用该 handler 函数,并将错误描述作为唯一参数传入。

condition-case 的行为相反,handler 是在错误发生的动态上下文中被调用的。这意味着它执行时不会解除任何变量绑定,也不会执行 unwind-protect 的任何清理操作,因此所有这些动态绑定仍然有效。 但有一个例外:在运行 handler 函数期间,从抛出错误的代码到当前 handler-bind 之间的所有错误处理函数都会被临时挂起。这意味着当再次抛出错误时,Emacs 只会查找位于 handler 函数内部,或当前 handler-bind 外部的那些生效的 condition-casehandler-bind。 另外请注意,词法绑定变量(see 词法绑定)不受影响,因为它们不具有动态作用域。

与任何普通函数一样,handler 可以非局部退出(通常通过 throw),也可以正常返回。如果 handler 正常返回,则表示该处理函数拒绝处理此错误,系统会从刚才中断的位置继续查找其他错误处理函数。

例如,如果我们想要记录某段代码执行过程中发生的所有错误,以及错误抛出时的当前缓冲区,同时又不影响这段代码原本的行为,就可以使用:

(handler-bind
    ((error
      (lambda (err)
        (push (cons err (current-buffer)) my-log-of-errors))))
  body-forms...)

这样只会记录那些没有在 body-forms… 内部被捕获、而是从其中 “逃逸(escape)” 出来的错误。并且它不会阻止这些错误继续传递给外层的 condition-case 处理函数(或 handler-bind 处理函数),因为上面的处理函数是正常返回的。

我们还可以使用 handler-bind 将一种错误替换成另一种。例如下面的代码,会把在 body-forms… 执行期间发生的所有 user-error 类型错误,都转换成普通的 error

(handler-bind
    ((user-error
      (lambda (err)
        (signal 'error (cdr err)))))
  body-forms...)

我们可以使用 condition-case 达到几乎相同的效果:

(condition-case err
    (progn body-forms...)
  (user-error (signal 'error (cdr err))))

但区别在于:当我们在 handler-bind 中(重新)抛出新错误时,原错误发生时的动态环境依然保持有效。 这意味着,例如如果此时进入调试器,它会显示完整的调用栈回溯,其中包含原始错误被抛出的位置:

Debugger entered--Lisp error: (error "Oops")
  signal(error ("Oops"))
  #f(lambda (err) [t] (signal 'error (cdr err)))((user-error "Oops"))
  user-error("Oops")
  ...
  eval((handler-bind ((user-error (lambda (err) ...

emacs

Emacs

org-mode

Orgmode

Donations

打赏

Copyright

© 2025 Jasper Hsu

Creative Commons

Creative Commons

Attribute

Attribute

Noncommercial

Noncommercial

Share Alike

Share Alike