0Pricing
Lua Academy · 课时

结构化错误对象

将表作为错误对象传递,以表达错误类型和上下文。

结构化错误对象 是 CoddyKit 上的免费 Lua Academy 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Lua Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Lua Academy 课程共包含 4 节课。

为什么使用结构化错误?

普通字符串错误难以通过程序处理。结构化错误对象(表)携带类型信息和上下文数据,调用方可以检查并据此采取行动。这样无需解析字符串即可根据错误进行分派。

-- Plain string: hard to handle programmatically
error("database error: connection refused")

-- Structured: type + data
error({
  type = "DatabaseError",
  code = "CONN_REFUSED",
  host = "localhost",
  port = 5432,
  message = "connection refused"
})

错误构造函数模式

请为每种错误类型创建一个错误工厂函数。该工厂应使用一致的字段构建表:类型、消息以及所有相关上下文。__tostring 元方法可以让错误打印得更清晰。

local ErrorMT = {__tostring = function(e)
  return string.format("[%s] %s", e.type, e.message)
end}

local function makeError(errType, msg, data)
  local e = {type=errType, message=msg}
  if data then for k,v in pairs(data) do e[k]=v end end
  return setmetatable(e, ErrorMT)
end

local E = {
  notFound = function(name) return makeError("NOT_FOUND","not found: "..name,{name=name}) end,
  badInput = function(msg,field) return makeError("BAD_INPUT",msg,{field=field}) end,
}

local ok, err = pcall(error, E.notFound("user:42"))
print(tostring(err))   -- [NOT_FOUND] not found: user:42

检查错误对象的类型

捕获错误后,请检查它是否为包含已知类型字段的表。这样,您就可以根据错误类型分派不同的恢复策略,而不依赖字符串模式匹配。

local function handleRequest(fn)
  local ok, err = pcall(fn)
  if ok then return true end
  if type(err) == "table" then
    if err.type == "NOT_FOUND" then
      print("404: " .. err.message)
    elseif err.type == "BAD_INPUT" then
      print("400: " .. err.message .. " (field: " .. (err.field or "?") .. ")")
    else
      print("500: unhandled error: " .. tostring(err))
    end
  else
    print("500: " .. tostring(err))
  end
  return false
end

错误层次结构

您可以通过检查 is_a 字段或使用元表来模拟错误层次结构。子错误类型会继承父类型的字段;对于不需要具体细节的代码而言,子错误也可以作为父类型处理。

local function isError(e, errType)
  if type(e) ~= "table" then return false end
  return e.type == errType or e.parentType == errType
end

local function makeDbError(code, msg)
  return {type="DbError:"..code, parentType="DbError", code=code, message=msg}
end

local err = makeDbError("TIMEOUT","query timed out")
print(isError(err, "DbError"))          -- true
print(isError(err, "DbError:TIMEOUT"))  -- true
print(isError(err, "NetworkError"))     -- false

包装错误

捕获并重新抛出错误时,请包装原始错误,以便添加上下文而不丢失原始信息。包装器拥有自己的类型,并将原始错误作为原因保存。

local function wrapError(msg, cause)
  return {
    type = "WrappedError",
    message = msg,
    cause = cause,
  }
end

local function loadConfig(path)
  local ok, err = pcall(function()
    local f = assert(io.open(path,"r"))
    local content = f:read("a")
    f:close()
    return content
  end)
  if not ok then
    error(wrapError("failed to load config: "..path, err))
  end
end

local ok2, e = pcall(loadConfig, "missing.cfg")
if not ok2 then
  print(e.message)
  print("Caused by:", tostring(e.cause))
end

错误代码与错误类型

常见的两种约定是:错误代码(数字,例如 HTTP 状态码)和错误类型字符串(语义名称)。错误代码便于进行数值比较;类型字符串能够自我说明。许多系统会同时使用二者。

local STATUS = {OK=200, NOT_FOUND=404, SERVER_ERROR=500, BAD_REQUEST=400}

local function makeStatusError(status, msg)
  return {status=status, message=msg, type="HTTPError"}
end

local function handleError(e)
  if e.status == STATUS.NOT_FOUND then
    print("Resource not found:", e.message)
  elseif e.status >= 500 then
    print("Server error:", e.message)
  else
    print("Error", e.status, e.message)
  end
end

handleError(makeStatusError(404, "user not found"))

错误栈(原因链)

当一个错误由另一个错误引起时,请将它们链接起来。这样可以完整呈现应用的每一层发生了什么问题。解除这条链即可记录或显示完整的错误经过。

local function unwindCause(e, depth)
  depth = depth or 0
  local pad = string.rep("  ", depth)
  if type(e) == "table" then
    print(pad .. (e.type or "Error") .. ": " .. (e.message or "?"))
    if e.cause then unwindCause(e.cause, depth+1) end
  else
    print(pad .. tostring(e))
  end
end

local inner = {type="IoError", message="permission denied"}
local outer = {type="ConfigError", message="cannot load config", cause=inner}
unwindCause(outer)
-- ConfigError: cannot load config
--   IoError: permission denied

回调上下文中的错误

当错误发生在回调(事件处理函数、迭代器)内部时,它会传播给回调的调用方。请使用 pcall 捕获错误,并报告哪个回调失败以及相关上下文。

local function runCallbacks(callbacks, data)
  local errors = {}
  for name, fn in pairs(callbacks) do
    local ok, err = pcall(fn, data)
    if not ok then
      errors[#errors+1] = {callback=name, error=err}
    end
  end
  return errors
end

local cbs = {
  validate = function(d) assert(d.name, "name required") end,
  transform = function(d) d.name = d.name:upper() end,
}

local errs = runCallbacks(cbs, {})
for _, e in ipairs(errs) do
  print(e.callback, "->", e.error)
end

打印错误详情

可以结构化且易读地打印错误对象的辅助函数,同时处理字符串错误和表错误。这在应用边界处很有用,因为错误通常会在那里被记录或展示给用户。

local function printError(err, prefix)
  prefix = prefix or "Error"
  if type(err) ~= "table" then
    print(prefix .. ": " .. tostring(err))
    return
  end
  print(prefix .. " [" .. (err.type or "unknown") .. "]")
  print("  Message: " .. (err.message or "?"))
  for k, v in pairs(err) do
    if k ~= "type" and k ~= "message" and k ~= "cause" then
      print("  " .. k .. ": " .. tostring(v))
    end
  end
  if err.cause then printError(err.cause, "  Caused by") end
end

使用结构化错误进行断言

请创建一个 assertT(使用类型错误进行断言),让它抛出结构化错误而不是普通字符串。这样调用方就可以方便地测试特定错误类型。

local function assertT(cond, errType, msg, data)
  if not cond then
    local e = {type=errType, message=msg}
    if data then for k,v in pairs(data) do e[k]=v end end
    error(e, 2)
  end
  return cond
end

local function createUser(name, age)
  assertT(type(name)=="string", "BAD_INPUT", "name must be string", {field="name"})
  assertT(age >= 0 and age <= 150, "BAD_INPUT", "invalid age", {field="age", value=age})
  return {name=name, age=age}
end

local ok, err = pcall(createUser, "Alice", -5)
if not ok then print(err.type, err.field, err.value) end

快速检查

将表传递给 error() 而不是字符串,主要优势是什么?

回顾:结构化错误

总结:

  • 将表传递给 error(),以创建结构化且可检查的错误
  • 包含:类型、消息以及相关上下文字段
  • 添加 __tostring 以生成易读的输出
  • 包装错误以添加上下文,同时不丢失原因
  • 在处理函数中按类型分派:检查 err.type,而不是字符串模式

常见问题解答

「结构化错误对象」课时是免费的吗?

是的 — 「结构化错误对象」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Lua Academy 课程的其余内容,请升级到 CoddyKit PRO。 Lua Academy 课程共包含 4 节课。

「结构化错误对象」这节课中我会学到什么?

将表作为错误对象传递,以表达错误类型和上下文。 你通过在浏览器中直接运行的动手代码来练习 Lua Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Lua Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Lua Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。

「结构化错误对象」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Lua Academy 课中编写并运行代码吗?

能。每节 Lua Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. error() 函数
  2. 使用 pcall 进行受保护调用
  3. xpcall 和消息处理器
  4. 结构化错误对象
← 返回 Lua Academy