Error Handling & Defensive Coding
How errors propagate in BeamNG Lua, pcall/xpcall patterns, nil safety, and preventing silent state corruption.
BeamNG's Lua VM is forgiving — errors in hooks don't crash the game. But that forgiveness is a trap: your extension silently fails, leaving state half-initialized or corrupted. Defensive coding prevents hours of debugging "it works sometimes" problems.
What Happens When an Extension Errors
When a hook function throws an error:
- The error is logged (with stack trace via StackTracePlus)
- The extension is NOT unloaded - it stays loaded with its other hooks still active
- Other extensions' hooks continue executing (errors don't stop propagation)
- The game does NOT crash - the Lua VM catches errors at the C++ boundary
This means a single buggy hook won't take down the game, but it WILL silently fail, potentially leaving state inconsistent.
pcall / xpcall
Use pcall to handle errors from code you don't fully trust (user input, file parsing, external data):
-- Basic pcall
local success, result = pcall(function()
return riskyOperation()
end)
if not success then
log('E', 'mymod', 'Operation failed: ' .. tostring(result))
end
-- xpcall with custom error handler (gets stack trace)
local success, result = xpcall(
function() return riskyOperation() end,
function(err) return debug.traceback(err) end
)Note: BeamNG replaces debug.traceback with StackTracePlus for better traces:
-- From main.lua:50
local STP = require "libs/StackTracePlus/StackTracePlus"
debug.traceback = STP.stacktrace
debug.tracesimple = STP.stacktraceSimpleNil Safety Patterns
The most common error in BeamNG Lua is indexing nil - a function returned nothing or an object doesn't exist.
-- ❌ Bad: Assumes everything exists
local veh = be:getPlayerVehicle(0)
local name = veh:getPath() -- CRASH if no player vehicle
-- ✅ Good: Nil check
local veh = be:getPlayerVehicle(0)
if veh then
local name = veh:getPath()
end
-- ✅ Good: Guard clause pattern
local function doVehicleThing()
local veh = be:getPlayerVehicle(0)
if not veh then return end
-- Safe to use veh
endCommon nil sources
be:getPlayerVehicle(0)- nil when no vehicle spawnedscenetree.findObject(name)- nil when object doesn't existmap.getMap()- may be nil before map loadsjsonReadFile(path)- nil if file doesn't exist or is invalid JSON- Any extension global (e.g.,
career_career) - nil if not loaded
JSON Safety
-- jsonReadFile returns nil on missing/invalid files
local data = jsonReadFile("settings/mymod.json")
if not data then
data = {default = true} -- Use defaults
end
-- jsonWriteFile can fail silently
local ok = jsonWriteFile("settings/mymod.json", data, true)
if not ok then
log('E', 'mymod', 'Failed to save settings')
endExtension Availability Checks
Never assume an extension is loaded:
-- ❌ Bad: Direct call without check
career_career.isActive()
-- ✅ Good: Check first
if career_career then
career_career.isActive()
end
-- ✅ Good: For optional integration
local function getCareerState()
if not career_career or not career_career.isActive then
return nil
end
return career_career.isActive()
endDefensive onExtensionLoaded
local function onExtensionLoaded(deserializedData)
-- Called only on THIS extension when it loads.
-- deserializedData contains state from a previous VM session (Ctrl+L reload).
-- Return false to abort loading.
local ok, err = pcall(initMyExtension, deserializedData)
if not ok then
log('E', 'mymod', 'Init failed: ' .. tostring(err))
return false -- Abort loading
end
endAnti-Pattern: Silent State Corruption
-- ❌ Bad: Error in onUpdate leaves inconsistent state
local processing = false
local function onUpdate(dtReal, dtSim, dtRaw)
if shouldProcess then
processing = true
doRiskyThing() -- If this errors, processing stays true forever
processing = false
end
end
-- ✅ Good: Use pcall to guarantee cleanup
local function onUpdate(dtReal, dtSim, dtRaw)
if shouldProcess then
processing = true
local ok = pcall(doRiskyThing)
processing = false -- Always runs
if not ok then
log('W', 'mymod', 'Processing failed, resetting state')
resetState()
end
end
endSee Also
- Debugging - Logging and inspecting data
- Anti-Patterns - Common mistakes to avoid
- Common Patterns - Defensive coding patterns