返回首页

教程导航

返回首页

🏷️ 标签(1)

🗂️ 分类(1)

第 14 章 错误处理

第 14 章 错误处理

Lua 的错误处理结合了返回值与异常式机制。可预期的失败可以返回 nil, message 或 nil, errorObject;无法继续执行的错误可以用 error 抛出。

error 与断言

local function divide(a, b)
  if b == 0 then
    error("division by zero", 2)
  end
  return a / b
end

error 的第二个参数是层级。1 表示在调用 error 的位置添加位置信息,2 表示指向上层调用者。封装校验函数时通常使用 2:

local function requireString(value, name)
  if type(value) ~= "string" then
    error(name .. " must be a string", 2)
  end
  return value
end

assert 在第一个参数为真时返回全部参数,否则抛出第二个参数:

local function openConfig(path)
  local file, message = io.open(path, "r")
  assert(file, message)
  return file
end

不要用 assert 取代所有业务校验。需要恢复或向用户展示具体字段错误时,应返回结构化错误。

pcall 与 xpcall

pcall 调用函数并捕获错误:

local function fail()
  error("boom")
end

local ok, message = pcall(fail)
print(ok)      -- false
print(message) -- 包含错误信息

成功时 pcall 返回 true 和函数的全部返回值:

local function pair()
  return 1, 2
end

local ok, first, second = pcall(pair)
print(ok, first, second)

xpcall 可以在错误传出前调用消息处理器,适合附加堆栈:

local function handleError(errorValue)
  return debug.traceback(tostring(errorValue), 2)
end

local ok, trace = xpcall(function()
  error("request failed")
end, handleError)

if not ok then
  io.stderr:write(trace, "\n")
end

Lua 5.2 及以后,xpcall 可以额外传入参数:

local ok, value = xpcall(string.rep, debug.traceback, "a", 3)
print(ok, value) -- true aaa

错误对象与错误层级

错误值不一定是字符串。结构化错误对象更容易被上层程序处理:

local function validationError(field, message)
  return {
    code = "VALIDATION_ERROR",
    field = field,
    message = message
  }
end

local ok, failure = pcall(function()
  error(validationError("email", "invalid format"))
end)

if not ok then
  print(failure.code)    -- VALIDATION_ERROR
  print(failure.field)   -- email
  print(failure.message) -- invalid format
end

错误对象不应包含密码、令牌或完整请求正文。日志中需要可追踪的请求 ID,同时保留用户可读的信息。

包装底层错误

local function loadConfig(path)
  local file, message = io.open(path, "r")
  if not file then
    error({
      code = "CONFIG_OPEN_FAILED",
      path = path,
      cause = message
    }, 0)
  end

  local content = file:read("*a")
  file:close()
  return content
end

level = 0 不添加位置前缀,适合消息已经是结构化对象的场景。

资源清理模式

pcall 中手动清理

local file, message = io.open("data.txt", "r")
if not file then
  error(message)
end

local ok, result = pcall(function()
  return file:read("*a")
end)

file:close()
if not ok then
  error(result, 0)
end

to-be-closed 变量

Lua 5.4 支持 <close> 变量。离开作用域时,包括发生错误时,会调用 __close:

local function withResource()
  local resource <close> = setmetatable({}, {
    __close = function(_, errorValue)
      if errorValue then
        print("清理并报告错误")
      else
        print("正常清理")
      end
    end
  })

  print("使用资源")
end

withResource()

自定义文件包装器也可以使用这一机制。__close 自身应尽量不抛错;如果清理失败,应记录并保留原始错误。

可观测的错误处理

生产代码应区分:

  • 用户错误:输入缺失、格式错误,返回可展示的信息。

  • 预期运行错误:网络断开、文件不存在,允许重试或降级。

  • 程序缺陷:不变量被破坏,应记录堆栈并尽快失败。

日志至少包含错误码、操作名称、相关资源标识和请求 ID,不要只输出 "failed"。

不要吞掉错误

-- 不推荐
pcall(function()
  saveData()
end)

-- 推荐
local ok, failure = pcall(saveData)
if not ok then
  logError("save failed", failure)
  return nil, failure
end

如果确实允许忽略错误,也要写明原因,例如清理临时文件失败不影响主流程。

上一章:协程 | 下一章:文件与输入输出