RLS Studios
ProjectsPatreonCommunityDocsAbout
Join Patreon
BeamNG Modding Docs

Guides

Lua Classes & MetatablesLua Coding StandardsProven GE PatternsAnti-Patterns to AvoidOverriding Lua ModulesCleaning Up AI-Generated CodeError 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 & Extension Checks

The most common error in BeamNG Lua is indexing nil. 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
  • Dynamically loaded extension globals (e.g., career_career) — nil if not loaded

For detailed patterns on nil-checking, extension availability guards, and when NOT to nil-check, see Anti-Patterns § Module Existence Checks and Anti-Patterns § Vehicle Spawning & Access.


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

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

Cleaning Up AI-Generated Code

How to identify and fix common AI coding patterns — excessive guards, restating comments, unnecessary boilerplate, and other slop that hurts readability.

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 & Extension ChecksJSON SafetyDefensive onExtensionLoadedAnti-Pattern: Silent State CorruptionSee Also