《elisp 每日一练》—— 错误处理

《elisp 每日一练》—— 错误处理

Masimaro
2026-10-28 / 0 评论 / 1 阅读 / 正在检测是否收录...

前面我们各种练习编写的都是一些小的功能,20到50行代码的练习即使不进行错误处理,也能够很方便的定位到问题。但是如果我们希望功能更加强大,甚至需要依赖大量外部package时,情况就变得复杂起来,而适当的错误处理和提示能更快的帮我们排查问题,恢复现场。今天我们来讨论Emacs中的错误处理

背景知识

error 和 user-erro 报告错误

Emacs代码出错时如果我们不干预,默认使用的是名为 error 的函数,它会终止程序并弹出调用的堆栈。它的签名如下:

(error STRING &rest ARGS)

它接收一个格式化的字符串,调用方式等同于 format 函数。例如下面的代码

(defun my-find-file(file)
  (interactive "Ffile path: ")
  (if (not (file-exists-p file))
      (error "invalid file path: %s" file)
    (find-file-noselect file)))

如果我们输入一个不存在的路径,那么它会终止程序并弹出调用栈,直接调用,会得到如下的输出

Debugger entered--Lisp error: (error "invalid file path: d:/test.txt")
  error("invalid file path: %s" "d:/test.txt")
  (if (not (file-exists-p file)) (error "invalid file path: %s" file) (find-file-noselect file))
  my-find-file("d:/test.txt")
  (progn (my-find-file "d:/test.txt"))
  eval((progn (my-find-file "d:/test.txt")) t)

这个信息对于调试来说非常有用,但是对于使用者来说却是不那么友好,如果想要更友好的信息,可以使用 user-error。它不会弹出这个恼人的错误栈信息,而是在底部输出信息并安全的终止程序。

给各位读者提个醒:验证它们的行为需要通过 M-x 交互式的方式调用,如果直接在buffer中通过 (my-find-file "1.txt") 这种形式二者的结果是一样的。都会显示调用栈。如果你发现交互式二者都不显示调用栈,有可能是当前 debug-on-error 的值为 nil 我们需要将它赋值为 t。就像它的名字一样,这个标志用来在出错时显示调用栈进行调试,elisp的文档中提到 user-error 并不是错误,它主要是向用户报告操作失误,例如上面例子中用户选择了打开不存在的文件。

condition-case 异常处理

如果是代码内部出错,例如读取的json或者xml文件不完整,网络波动导致收包失败等等情况,就不合适使用 error这种函数来报告错误,在其他语言中一般采用的是try-catch 的方式,elisp中也有类似的机制。elisp中使用 condition-case

(condition-case VAR BODYFORM &rest HANDLERS)

如果我们类比其他语言中的异常处理,可以将上面的各个部分组织成

try
{
    BODYFORM
}
catch (exception VAR)
{
    HANDLERS
}

根据这个结构,VAR 是代表异常的变量,它的结构为 (error-symbol . error-data) , error-data 是一个list,但是并没有详细规定list中的数据,因此我们无法事先判断list中各个数据成员。好在一般我们不需要通过这个结构来获取详细信息,可以使用 error-message-string。

与其他语言类似,异常可以有不同的类型,也可以有对应的继承关系, 例如 file-error 下有具体的"派生类":file-missing、file-already-exists。而所有异常的"基类" 是 error , 这些类型对应着前面提到的异常结构中的 error-symbol 。它们只是一个符号并不是具体的数据结构。

上面将它称为异常并不准确,它不仅仅可以捕获程序中出现的问题,而是可以捕获所有通过 signal 函数抛出的“条件”。我们可以通过 signal 函数抛出错误或者终止信息。为了照顾初学者,方便理解我们下文仍然采用异常这个称呼吧

对于错误来说,我们使用 error 就能全部捕获但是对于用户操作的错误来说,例如用户手动终止程序,就不属于错误,这种情况我们要使用 'quit

如果你想一劳永逸捕获所有情况,直接在 HANDLERS 中使用 t 即可。

BODYFORM 是需要被包裹的真正执行的代码。

HANDLERS 是具体处理异常的代码块。它的结构大致是 (error-symbol body)。表示 error-symbol 之类的错误交给 body 块处理。它对应着我们前面提到的异常结构 (error-symbol . error-data) 中的 error-symbol

(condition-case err
    ;; 1. 保护区:可能会抛出错误的危险代码
    (progn
      (json-parse-string invalid-json-string))
  
  ;; 2. 错误处理器:当捕获到错误时执行
  (error
   ;; err 的结构通常为 (error-symbol . error-data)
   (message "捕获到运行时错误: %s" (error-message-string err))
   ;; 返回一个降级后的默认值
   nil))

我们可以多次使用这个结构,多次捕获不同类型的异常。但是与其他编程语言中的顺序类似,越是包含范围广的类型越要放在后面。例如 error 应该放在 file-missing 或者 error-symbol 这种错误之后

signal 抛出异常

在其他语言中,异常处理还有一个 throw 抛出异常的操作。我们可以使用 error 和 user-error。它们的运作原理与其他语言中的抛出异常类似,当通过 error 和 user-error 触发异常之后,会一级级往上找,看看是否有异常处理。如果被处理掉了,异常传递终止。如果直到最后没有被处理,则调用默认的异常处理,此时就会触发显示调用栈并终止程序的操作。

其中 error 函数抛出异常类型中的 'error,user-error 会抛出 'user-error 类型的错误,根据继承关系,我们也可以通过 'error 来捕获。

抛出异常,我们还可以使用更底层的 signal 函数。它的原形如下:

(signal ERROR-SYMBOL DATA)

第一个参数是错误的符号,第二个参数是错误的数据,数据是一个list,后续异常处理时可以使用这个list中的数据。例如上面的 my-find-file 可以进行改写

(defun my-find-file(file)
  (interactive "Ffile path: ")
  (if (not (file-exists-p file))
      (signal 'file-missing (list file))
    (find-file-noselect file)))

(condition-case err
    (my-find-file "d:/test")
  (file-missing
   (message "%s" (error-message-string err))))

unwind-protect

这个函数类似于异常处理体系中的 finally,它保证无论如何清理资源的操作都会执行。它的原形如下:

(unwind-protect BODYFORM UNWINDFORMS...)

BODYFORM 是真实执行业务层的代码,而 UNWINDFORMS 可以是多个语句,用来进行最终的清理工作。我们还可以与 condition-case 搭配使用,以达到其他语言中 try-catch-finally 的效果:

(unwind-protect
    (condition-case err
        (progn
          ;; try 块
          (my-risky-operation))
      (error
       ;; catch 块
       (message "捕获到错误: %s" (error-message-string err))))
  ;; finally 块:无论 try 成功、抛错被捕获、还是错误穿透,都会执行
  (cleanup-resources))

总结

本篇我们讨论了Elisp中异常处理,涉及的函数有:

  • error
  • user-error
  • condition-case
  • error-message-string
  • signal
  • unwind-protect
0

评论 (0)

取消