18.错误处理
18.1 知识点
错误处理的基本概念
- Lua 脚本运行时如果出现错误,默认会直接中断当前执行流程。
- 基础阶段先掌握几个常用方法:
errorassertpcallxpcalldebug.traceback
- 这篇只讲最小可用写法,目标是知道怎么主动抛错、怎么检查条件、怎么避免脚本报错后直接把主流程打断。
项目里写 Lua,最怕的是一个小脚本报错把后面的逻辑全断了。
所以错误处理不是为了“吞掉错误”,而是为了让错误有地方被接住,并且能打出足够的信息方便排查。
error主动抛出错误
error用来主动抛出错误。- 语法:
error("错误信息")
- 示例:
function TestError()
error("这里主动抛出一个错误")
end
TestError()
print("这行不会执行")
运行到 error 后,当前流程会被中断。
所以上面最后一行 print 不会执行。
实际项目里,error 一般用在“不应该继续跑”的地方,比如参数非法、配置缺字段、状态不对。
function CreatePlayer(playerId)
if playerId == nil then
error("CreatePlayer失败,playerId不能为空")
end
print("创建角色:" .. playerId)
end
CreatePlayer(nil)
这里不要为了省事到处 error。
普通业务失败更常见的是返回 false, reason,真正遇到不该继续的异常情况,再考虑抛错。
assert断言
assert可以理解成“条件检查”。- 如果第一个参数为真,就继续执行。
- 如果第一个参数为
false或nil,就抛出错误。 - 语法:
assert(条件, "错误信息")
- 示例:
local playerId = nil
assert(playerId ~= nil, "playerId不能为空")
print("这里不会执行")
assert 常用来检查函数参数:
function LoadConfig(configName)
assert(configName ~= nil, "configName不能为空")
print("加载配置:" .. configName)
end
LoadConfig("PlayerConfig")
LoadConfig(nil)
assert 比手写 if + error 更短,适合做一些基础前置检查。
但它不是万能校验系统,复杂的业务校验还是正常写 if / else 更清楚。
pcall保护调用
pcall可以保护一次函数调用。- 被
pcall包起来的函数如果报错,不会直接中断外层主流程。 pcall会返回两个主要结果:- 第一个返回值:是否执行成功。
- 后面的返回值:成功时是函数返回值,失败时是错误信息。
pcall调用成功
function Add(a, b)
return a + b
end
local isSuccess, result = pcall(Add, 1, 2)
print(isSuccess) -- true
print(result) -- 3
pcall调用失败
function TestPCallError()
error("TestPCallError里出错了")
end
local isSuccess, errorMessage = pcall(TestPCallError)
print(isSuccess) -- false
print(errorMessage) -- TestPCallError里出错了
print("pcall接住错误后,外层流程还可以继续执行")
pcall 的价值在这里:
里面的函数炸了,外面还能继续跑。比如加载某个可选配置、执行某个脚本回调、跑一段活动逻辑时,可以先用 pcall 兜住,避免一个错误直接打断整条主流程。
不过也别把 pcall 当成万能保险。
它能接住错误,不代表错误就不用修。线上代码如果只是把错误吞掉,后面排查会更麻烦。
pcall传参和接收返回值
pcall后面可以继续传参数。- 这些参数会传给被保护调用的函数。
- 如果函数正常返回,
pcall后面的返回值就是函数本身的返回值。
function Divide(a, b)
if b == 0 then
error("除数不能为0")
end
return a / b
end
local isSuccess, result = pcall(Divide, 10, 2)
print(isSuccess) -- true
print(result) -- 5
如果函数内部报错:
local isSuccess, result = pcall(Divide, 10, 0)
print(isSuccess) -- false
print(result) -- 除数不能为0
这种写法很适合包一层危险逻辑。
比如外部配置不可信、脚本回调不确定、数据可能为空时,至少可以先保证主流程别直接崩掉。
xpcall自定义错误处理
xpcall和pcall类似,也可以保护函数调用。- 区别是:
xpcall可以额外传入一个错误处理函数。 - 被保护的函数如果正常执行,错误处理函数不会执行。
- 被保护的函数如果报错,才会进入错误处理函数。
- 错误处理函数的参数,就是原始错误信息。
- 错误处理函数的返回值,会作为
xpcall失败时的第二个返回值。 - 基础阶段最常见的用法,就是配合
debug.traceback打出调用栈。
语法可以先这样记:
xpcall(要执行的函数, 错误处理函数)
xpcall调用失败
function TestXPCallError()
error("TestXPCallError里出错了")
end
function ErrorHandler(errorMessage)
print("捕获到错误:" .. tostring(errorMessage))
return "处理后的错误:" .. tostring(errorMessage)
end
local isSuccess, result = xpcall(TestXPCallError, ErrorHandler)
print(isSuccess) -- false
print(result) -- 处理后的错误:TestXPCallError里出错了
这个例子的执行顺序是:
1. xpcall 开始执行 TestXPCallError
2. TestXPCallError 内部调用 error 抛出错误
3. xpcall 捕获到错误,不让错误继续往外抛
4. xpcall 调用 ErrorHandler,并把原始错误信息传进去
5. ErrorHandler 里面的 print 会执行,所以会打印“捕获到错误:xxx”
6. ErrorHandler return 的内容,会变成 xpcall 的第二个返回值 result
7. xpcall 返回 false, result
8. 外层流程继续执行
所以这句:
print("捕获到错误:" .. tostring(errorMessage))
会不会打印,取决于 TestXPCallError 里面有没有报错。
如果被保护函数没有报错,ErrorHandler 根本不会执行。
xpcall调用成功
function TestXPCallSuccess()
return "执行成功"
end
function ErrorHandler(errorMessage)
print("捕获到错误:" .. tostring(errorMessage))
return errorMessage
end
local isSuccess, result = xpcall(TestXPCallSuccess, ErrorHandler)
print(isSuccess) -- true
print(result) -- 执行成功
这个例子里,TestXPCallSuccess 没有报错,所以 ErrorHandler 不会执行。
因此不会打印“捕获到错误:xxx”。
xpcall传参
Lua 5.2+ 里,xpcall 通常可以在错误处理函数后面继续传参:
function Divide(a, b)
if b == 0 then
error("除数不能为0")
end
return a / b
end
function ErrorHandler(errorMessage)
print("捕获到错误:" .. tostring(errorMessage))
return errorMessage
end
local isSuccess, result = xpcall(Divide, ErrorHandler, 10, 2)
print(isSuccess) -- true
print(result) -- 5
如果参数会导致报错:
local isSuccess, result = xpcall(Divide, ErrorHandler, 10, 0)
print(isSuccess) -- false
print(result) -- 除数不能为0
执行顺序还是一样:
先执行 Divide(10, 0),里面报错后,才会调用 ErrorHandler(errorMessage)。
不过要注意版本差异。Unity Lua 热更项目里常见 Lua 5.1 / LuaJIT 环境,这类环境下 xpcall 不一定支持在后面直接传参数。更稳的写法是用匿名函数包一层:
local isSuccess, result = xpcall(function()
return Divide(10, 0)
end, ErrorHandler)
print(isSuccess) -- false
print(result) -- 除数不能为0
这种写法虽然多包了一层,但兼容性更好,也更直观:xpcall 只负责执行这个匿名函数,真正的参数在匿名函数里面自己传。
这里 ErrorHandler 不是拿来修复错误的。
它更常见的作用是整理错误信息、打印日志、补充调用栈。
debug.traceback简单用法
debug.traceback可以生成调用栈信息。- 基础阶段只用它配合
xpcall看报错位置即可。 - 先不展开
debug.getinfo / debug.getupvalue / debug.sethook,这些放到 Lua 进阶知识里再整理。
function A()
B()
end
function B()
C()
end
function C()
error("C函数里出错了")
end
function ErrorHandler(errorMessage)
return debug.traceback(errorMessage)
end
local isSuccess, errorMessage = xpcall(A, ErrorHandler)
print(isSuccess)
print(errorMessage)
大概会打印类似这样的内容:
false
...arning\HotFix\Lua\LuaBase\LuaBase\Lesson18_Error.lua:132: C函数里出错了
stack traceback:
...arning\HotFix\Lua\LuaBase\LuaBase\Lesson18_Error.lua:138: in function <...arning\HotFix\Lua\LuaBase\LuaBase\Lesson18_Error.lua:135>
[C]: in function 'error'
...arning\HotFix\Lua\LuaBase\LuaBase\Lesson18_Error.lua:132: in function 'C'
...arning\HotFix\Lua\LuaBase\LuaBase\Lesson18_Error.lua:128: in function 'B'
...arning\HotFix\Lua\LuaBase\LuaBase\Lesson18_Error.lua:124: in function <...arning\HotFix\Lua\LuaBase\LuaBase\Lesson18_Error.lua:123>
[C]: in function 'xpcall'
...arning\HotFix\Lua\LuaBase\LuaBase\Lesson18_Error.lua:141: in main chunk
[C]: ?
具体输出格式不同 Lua 版本、不同宿主环境里可能不完全一样。
实际项目里更重要的是:能看到错误信息和大概调用路径,方便定位是哪一层调用炸了。
debug.traceback 一般放在 xpcall 的错误处理函数里用。
这个时机比较合适,因为错误刚被捕获,调用栈信息还比较完整。
function ErrorHandler(errorMessage)
local traceback = debug.traceback(errorMessage)
print(traceback)
return traceback
end
如果在错误处理函数里已经 print(traceback),外层又 print(result),就会打印两次。
实际项目里一般二选一,不要重复打太多日志。
pcall和xpcall怎么选
- 只是想保护一段函数调用,不让错误直接中断外层流程,用
pcall就够了。 - 想在报错时统一加工错误信息,或者顺手带上调用栈,用
xpcall更合适。 - 简单脚本里不用到处包,关键入口、外部脚本回调、配置加载这类不稳定位置再包。
-- 简单保护
local ok, result = pcall(SomeFunction)
-- 想带调用栈
local ok, err = xpcall(SomeFunction, function(errorMessage)
return debug.traceback(errorMessage)
end)
这里要注意:错误处理不是为了把错误藏起来。
更好的习惯是把错误接住、打清楚,然后决定当前流程是否还能继续。
总结
error:主动抛出错误。assert:条件不成立时抛出错误,适合做简单参数检查。pcall:保护函数调用,防止错误直接中断外层流程。xpcall:比pcall多一个错误处理函数。ErrorHandler:只在被保护函数报错时执行。ErrorHandler的返回值:会变成xpcall失败时的第二个返回值。xpcall传参:新版本 Lua 可以直接传,Lua 5.1 / LuaJIT 项目更稳的是匿名函数包一层。debug.traceback:生成调用栈信息,基础阶段常和xpcall搭配使用。debug.getinfo / debug.getupvalue / debug.sethook / profiling不放在基础篇,后面放到 Lua 进阶知识的错误与调试里整理。
18.2 知识点代码
Lesson18_错误处理.lua
print("**********错误处理************")
print("**********知识点一 error主动抛出错误************")
-- error 用来主动抛出错误
-- 运行到 error 后,当前执行流程会被中断
-- function TestError()
-- error("这里主动抛出一个错误")
-- end
-- TestError()
-- print("这行不会执行")
print("**********知识点二 assert断言************")
-- assert 可以理解成条件检查
-- 如果第一个参数为真,就继续执行
-- 如果第一个参数为 false 或 nil,就抛出错误
function LoadConfig(configName)
assert(configName ~= nil, "configName不能为空")
print("加载配置:" .. configName)
end
LoadConfig("PlayerConfig")
-- LoadConfig(nil) -- 会报错:configName不能为空
print("**********知识点三 pcall保护调用************")
function Add(a, b)
return a + b
end
local isSuccess, result = pcall(Add, 1, 2)
print(isSuccess) -- true
print(result) -- 3
function TestPCallError()
error("TestPCallError里出错了")
end
local isSuccess, errorMessage = pcall(TestPCallError)
print(isSuccess) -- false
print(errorMessage) -- TestPCallError里出错了
print("pcall接住错误后,外层流程还可以继续执行")
print("**********知识点四 pcall传参和接收返回值************")
function Divide(a, b)
if b == 0 then
error("除数不能为0")
end
return a / b
end
local isSuccess, result = pcall(Divide, 10, 2)
print(isSuccess) -- true
print(result) -- 5
local isSuccess, result = pcall(Divide, 10, 0)
print(isSuccess) -- false
print(result) -- 除数不能为0
print("**********知识点五 xpcall自定义错误处理************")
function TestXPCallError()
error("TestXPCallError里出错了")
end
function ErrorHandler(errorMessage)
print("捕获到错误:" .. tostring(errorMessage))
return "处理后的错误:" .. tostring(errorMessage)
end
local isSuccess, result = xpcall(TestXPCallError, ErrorHandler)
print(isSuccess) -- false
print(result) -- 处理后的错误:TestXPCallError里出错了
function TestXPCallSuccess()
return "执行成功"
end
local isSuccess, result = xpcall(TestXPCallSuccess, ErrorHandler)
print(isSuccess) -- true
print(result) -- 执行成功
-- 这次不会打印“捕获到错误”,因为 TestXPCallSuccess 没有报错
print("**********知识点六 xpcall传参************")
-- Lua 5.2+ / Lua 5.4 可以在 xpcall 后面直接传参数
-- 但 Unity Lua 热更项目里常见 Lua 5.1 / LuaJIT
-- 为了兼容这类环境,更稳的是用匿名函数包一层
local isSuccess, result = xpcall(function()
return Divide(10, 0)
end, ErrorHandler)
print(isSuccess) -- false
print(result) -- 处理后的错误:除数不能为0
print("**********知识点七 debug.traceback简单用法************")
function A()
B()
end
function B()
C()
end
function C()
error("C函数里出错了")
end
function TracebackHandler(errorMessage)
-- debug.traceback 可以生成调用栈信息
-- 这里先只用来辅助查看错误位置
return debug.traceback(errorMessage)
end
local isSuccess, errorMessage = xpcall(A, TracebackHandler)
print(isSuccess) -- false
print(errorMessage) -- 会带上错误信息和调用栈
print("**********知识点八 pcall和xpcall怎么选************")
-- 只是想保护一段函数调用,不让错误直接中断外层流程,用 pcall 就够了
-- 想在报错时统一加工错误信息,或者顺手带上调用栈,用 xpcall 更合适
-- 错误处理不是为了把错误藏起来
-- 更好的习惯是把错误接住、打清楚,然后决定当前流程是否还能继续
local ok1, ret1 = pcall(Add, 10, 20)
print(ok1, ret1) -- true 30
local ok2, ret2 = xpcall(function()
return Divide(10, 0)
end, function(errorMessage)
return debug.traceback(errorMessage)
end)
print(ok2)
print(ret2)
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com