Функции pcall, xpcall, error и assert: обработка и генерация ошибок в Lua
Опубликовано:
В Lua нет оператора для перехвата исключений как, например, try-except в Python. Вместо этого используется функциональный подход — встроенные pcall и xpcall. Поэтому, чтобы запустить код в защищенном режиме, его нужно обернуть в функцию, которую передать в pcall или xpcall, то есть выполнить так называемый изолированный вызов (protected call).
В статье сначала дается краткий обзор типичных ошибок в Lua, далее объясняется работа pcall и xpcall, рассказывается как принудительно создавать ошибки (error — безусловная генерация, assert — условная), в конце приводится пример перехвата сгенерированных ошибок с помощью pcall.
В Lua обычно говорят об ошибках, а не исключениях, так как в этом языке для обработки сбоев нет специальных классов и объектов-исключений. В Lua любая внештатная ситуация — это либо ошибка времени выполнения (runtime error), либо ошибка синтаксиса (syntax error). Ошибка в Lua — это, как правило, обычная строка текста, которая сообщает, что пошло не так.
Одной из наиболее частых является обращение к полю несуществующей таблицы, когда вместо нее в переменной содержится nil:
local t = nil print(t.field)
attempt to index a nil value (local 't')
В приведенном выше коде ошибка очевидна на момент написания. Однако если данные переменная должна получать извне в процессе выполнения программы, есть вероятность, что в итоге она получит nil.
Попытка вызова несуществующей функции обычно возникает при работе с модулями, если вы забыли ее экспортировать.
local player = {x=10, y=10, jump=100} print(player.jump())
attempt to call a number value (field 'jump')
В примере в выводе информации об ошибке вместо "a number value" было бы "a nil value", если бы идентификатора jump в принципе не было бы в коде.
В Lua если в конкатенации участвует nil или таблица, не имеющая метаметода __tostring, возникает ошибка:
print(nil .. 'Bob')
attempt to concatenate a nil value
В случае арифметических операций если в строке окажутся буквы, или вы попытаетесь совершить математическое действие с nil или таблицей, произойдет сбой:
local a = 'Ten' a = a + 10
attempt to add a 'string' with a 'number'
local a = io.read('n') print(a + 10)
Ten lua: file.lua:2: attempt to perform arithmetic on a nil value (local 'a')
Бывают ошибки передачи неверного типа в функции (type mismatch). Так многие встроенные функции Lua строго относятся к типам аргументов. Например, функция math.sin() ожидает число, а table.insert() — таблицу на первом месте.
local t = {10, 30} table.insert(t, 0) -- работает local p = 10 table.insert(p, 0) -- не работает
bad argument #1 to 'insert' (table expected, got number)
Посмотрим, как в Lua можно обработать такие ошибки с помощью встроенной функции pcall, которая специально для этого предназначена:
-- Имитируем ситуацию: данные не загрузились (переменная равна nil) local user = nil -- local user = {name='Bob', age=30} -- Опасная функция, которая гарантированно упадет без защиты local function print_name(profile) -- Если profile равен nil, здесь произойдет ошибка return profile.name end -- ВЫЗОВ ПОД ЗАЩИТОЙ (pcall) -- Передаем саму функцию и аргумент для нее через запятую local success, result = pcall(print_name, user) -- Анализируем результат работы pcall if success then print('Имя пользователя: ' .. result) else print('🚨 Не удалось прочитать имя пользователя.') print('Текст ошибки из консоли: ' .. result) end -- Этот код выполнится в любом случае, т.к. pcall изолировал сбой print('Выполнение скрипта продолжается дальше...')
🚨 Не удалось прочитать имя пользователя. Текст ошибки из консоли: 2.lua:8: attempt to index a nil value (local 'profile') Выполнение скрипта продолжается дальше...
Если раскомментировать строку, где user связан с таблицей, то вывод будет такой:
Имя пользователя: Bob Выполнение скрипта продолжается дальше...
Функция pcall всегда возвращает как минимум одно значение — "флаг успешности". Второе возвращаемое значение — это либо то, что возвращает функция, либо строка с текстом ошибки (системный трейс). Если функция возвращает несколько значений, то переменных до знака равенства надо указывать больше. В случае ошибки лишним присвоится nil.
Обычный защищенный вызов с помощью pcall сообщает только, в каком месте произошла ошибка. Однако если функция глубоко вложена, вы не узнаете, какой именно путь к ней привел, так как стек вызовов к моменту возврата из pcall уже уничтожен. Поэтому для отслеживания трека вызовов используется функция xpcall:
-- Функция обработки ошибок (Error Handler). -- Вызывается ДО того, как Lua развернет стек, поэтому видит всю цепочку вызовов. local function error_handler(err) -- debug.traceback() сам собирает лог вызовов return debug.traceback('🚨 Произошла ошибка: ' .. tostring(err), 2) end -- Имитируем глубокую вложенность вызовов local function database_query(profile) -- Последний вызов -- Падение здесь: profile равен nil return profile.name end local function get_user_data(profile) return database_query(profile) .. '!' -- Вызов второго уровня end local function render_profile_page(profile) return get_user_data(profile) .. '!' -- Вызов первого уровня end -- ВЫЗОВ ПОД РАСШИРЕННОЙ ЗАЩИТОЙ (xpcall) -- Передаем: 1. Целевую функцию, 2. Обработчик ошибок, 3. Аргументы local success, result = xpcall(render_profile_page, error_handler, nil) if not success then print(result) -- Выводим подробный отчет со стеком вызовов end print('Скрипт успешно работает дальше...')
🚨 Произошла ошибка: file.lua:11: attempt to index a nil value (local 'profile') stack traceback: file.lua:11: in upvalue 'database_query' file.lua:15: in upvalue 'get_user_data' file.lua:19: in function <file.lua:18> [C]: in function 'xpcall' file.lua:24: in main chunk [C]: in ? Скрипт успешно работает дальше...
В реальных проектах (например, в логировании на сервере или в игровом движке) в качестве второго аргумента xpcall почти всегда просто передают встроенную функцию debug.traceback:
local success, result = xpcall(render_profile_page, debug.traceback, nil)
В самописном обработчике error_handler мы передавали в debug.traceback() аргумент 2, чтобы скрыть сам обработчик ошибок из отчета и начать лог сразу с той строки кода, где произошла ошибка.
В Lua для принудительной генерации ошибок, когда надо прервать естественный ход выполнения программы, используется базовая функция error.
-- print( 10 / 0) -- Вывод: inf local function divide(a, b) if b == 0 then -- Говорим, что ошибка произошла на уровне 2 (у того, кто вызвал divide) error('Ошибка: Деление на ноль невозможно!', 2) end return a / b end print(divide(10, 0))
lua: 4.lua:11: Ошибка: Деление на ноль невозможно! stack traceback: [C]: in function 'error' 4.lua:6: in local 'divide' 4.lua:11: in main chunk [C]: in ?
Когда вызывается error, Lua останавливает текущую функцию и начинает "разворачивать" стек вызовов вверх, пока не встретит защищенный вызов pcall или xpcall. Если их нет, скрипт полностью завершает работу с выводом ошибки.
Кроме этого, в Lua есть функция assert, которая в простых случаях удобнее error, так как избавляет от необходимости использования условий. Две функции ниже работают одинаково:
local function set_age1(age) if not age then error('Возраст не указан!') end print('Возраст установлен: ' .. age) end local function set_age2(age) -- Если age равен nil или false, выбросится ошибка assert(age, 'Возраст не указан!') print('Возраст установлен: ' .. age) end -- set_age1(false) set_age2(false)
lua: 5_assert.lua:10: Возраст не указан! stack traceback: [C]: in function 'assert' 5_assert.lua:10: in local 'set_age2' 5_assert.lua:15: in main chunk [C]: in ?
Однако первая более универсальная, так как вторая не подойдет, если проверять age надо на попадание в диапазон. Функция assert может быть удобна в комбинации io.open, которая в случае неудачи открытия файла не роняет скрипт, а возвращает nil и текст ошибки. В примере ниже ошибка могла бы появиться только на этапе чтения файла, если бы не условный оператор:
local file = io.open('test.txt', 'r') if file then local content = file:read('*a') file:close() print(content) else print('Не удалось открыть файл') end
Когда же ошибка должна генерироваться на этапе открытия, используют assert:
-- Если файл не откроется, assert получит nil и сообщение об ошибке от io.open local file = assert(io.open('test.txt', 'r'))
Функции error и assert полезны, когда пишется внутренняя логика функции (валидация, вычисления), чтобы сразу выйти из нее при неподходящих данных. Однако даже самостоятельно созданную ошибку будет правильным перехватывать с помощью pcall или xpcall. Пример:
-- Функция с проверками (может выбросить ошибку) local function load_player_data(nickname) -- 1. Быстрая проверка через assert assert(nickname, 'Никнейм не может быть пустым!') -- Имитируем, что игрока 'Admin' нельзя загрузить по соображениям безопасности if nickname == 'Admin' then -- 2. Сложная логика с явным error error('Доступ к Admin заблокирован!', 2) end return { name = nickname, level = 80 } end -- ======================================================== -- ЗАЩИЩЕННЫЙ ВЫЗОВ (pcall) -- ======================================================== -- Сценарий А: Передаем nil (сработает assert) local success, result = pcall(load_player_data, nil) print('Статус А:', success) -- false print('Ошибка А:', result) -- 'file.lua:4: Никнейм не может быть пустым!' -- Сценарий Б: Передаем 'Admin' (сработает error) local success, result = pcall(load_player_data, 'Admin') print('Статус Б:', success) -- false print('Ошибка Б:', result) -- 'Доступ к Admin заблокирован!' -- Сценарий В: Все хорошо local success, data = pcall(load_player_data, 'Bob') print('Статус В:', success) -- true print('Имя игрока:', data.name) -- Bob