RLS Studios
ProjectsPatreonCommunityDocsAbout
Join Patreon
BeamNG Modding Docs

Guides

Lua Classes & MetatablesProven GE PatternsAnti-Patterns to AvoidError Handling & Defensive CodingPerformance OptimizationLua Modding FAQ

Reference

UI

Resources

BeamNG Game Engine Lua Cheat SheetGE Developer RecipesMCP Server Setup

// RLS.STUDIOS=true

Premium Mods for BeamNG.drive. Career systems, custom vehicles, and immersive gameplay experiences.

Index

HomeProjectsPatreon

Socials

DiscordPatreon (RLS)Patreon (Vehicles)

© 2026 RLS Studios. All rights reserved.

Modding since 2024

GuidesLua

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.stacktraceSimple

Nil 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
end

Common nil sources

  • be:getPlayerVehicle(0) - nil when no vehicle spawned
  • scenetree.findObject(name) - nil when object doesn't exist
  • map.getMap() - may be nil before map loads
  • jsonReadFile(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')
end

Extension 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()
end

Defensive 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
end

Anti-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
end

See Also

  • Debugging - Logging and inspecting data
  • Anti-Patterns - Common mistakes to avoid
  • Common Patterns - Defensive coding patterns

Anti-Patterns to Avoid

Common BeamNG modding mistakes that cause crashes, data loss, and silent failures — with correct alternatives for each.

Performance Optimization

How to keep your BeamNG Lua mod fast — avoiding GC pressure, caching lookups, efficient string handling, and proper timer patterns.

On this page

What Happens When an Extension Errorspcall / xpcallNil Safety PatternsCommon nil sourcesJSON SafetyExtension Availability ChecksDefensive onExtensionLoadedAnti-Pattern: Silent State CorruptionSee Also