В Lua штатный механизм ошибок - это пара error() (бросить) и pcall() (поймать). Tarantool строит поверх этого свою систему: модуль box.error, объект ошибки error_object (по факту cdata, а не строка) и глобальный слот "последней ошибки" - box.error.last(). Этот урок про то, КАК ИМЕННО ошибка рождается, распространяется по стеку, ловится и диагностируется - от Lua-исключения до объекта с кодом, типом, сообщением и трейсбеком, который можно прокинуть через сеть net.box без потери смысла.
Ключевая мысль: в Tarantool ошибка - это не текст, а структура. Если относиться к ней как к строке, теряются код, тип и цепочка причин - именно на этом спотыкается большинство новичков.
Механика: как устроено внутри
1. Два слоя - Lua и box
Голый Lua знает только error(value) и pcall(f). Значением ошибки может быть что угодно: строка, таблица, число. Когда Tarantool бросает свою ошибку (нарушение уникальности, NO_SUCH_USER, READONLY и т.п.), значением является error_object - C-объект (cdata struct error) с подсчётом ссылок. У него есть поля code, type, message, trace, а также метаметод __tostring, поэтому в консоли он печатается как обычное сообщение, хотя внутри это структура.
2. Слот last - дополнительный канал диагностики
Параллельно с возвратом значения через pcall Tarantool сохраняет последнюю поднятую ошибку в потоко-локальном (на файбер) слоте. Достать её можно box.error.last(), очистить - box.error.clear(), а установить вручную - box.error.set(err). Это удобно, когда ошибка прошла через слой, который вернул просто nil: первопричину всё ещё видно через last(). Слот привязан к файберу, поэтому конкурентные запросы не затирают чужие ошибки.
3. Цепочки причин (error chaining)
С версии 2.4.1 у объекта есть set_prev(err) и поле prev. Это позволяет строить цепь e1 -> e2 -> e3: низкоуровневая ошибка хранится как причина высокоуровневой, и при печати/распаковке видно всю историю. В Tarantool 3.1 добавили сахар - prev прямо в конструкторе: box.error.new({type=..., message=..., prev=lower_err}). Это аналог "exception chaining" в больших языках.
4. Трейсбек собирается лениво
Сбор Lua-трейсбека стоит денег, поэтому он управляется через box.error.cfg{traceback_enable = true/false}. Когда включено, в момент, когда объект ошибки впервые показывается пользователю, в поле trace заполняется список кадров (файл, строка). По сети (net.box) ошибка кодируется как MsgPack-расширение MP_ERROR (опция msgpack.cfg.encode_error_as_ext, включена по умолчанию), но локальный Lua-трейсбек удалённой стороны НЕ переносится - вы получаете код, тип, сообщение и серверный C-trace, а не Lua-стек вызвавшего файбера.

Поток обработки ошибок и диагностики в Tarantool
Ключевые команды и код
Поймать и разобрать ошибку
Код: Выделить всё
local ok, err = pcall(function()
box.space.users:insert{1, 'Ann'}
box.space.users:insert{1, 'Bob'} -- дубль первичного ключа
end)
print(ok) -- false
print(type(err)) -- cdata, а НЕ string
print(err.code) -- 3 (ER_TUPLE_FOUND)
print(err.type) -- ClientError
print(err.message) -- Duplicate key exists ...
-- полный разбор одним вызовом:
local t = err:unpack()
-- t = {code=3, type='ClientError', message='...', trace={...}}
Код: Выделить всё
-- new() создаёт объект, но НЕ бросает
local e = box.error.new({ code = 1001, type = 'MyAppError',
reason = 'budget exceeded' })
-- бросить можно тремя способами:
box.error(e) -- поднять готовый объект
-- или
e:raise()
-- или сразу одним вызовом без промежуточного объекта:
box.error({ reason = 'budget exceeded', type = 'MyAppError' })
Код: Выделить всё
-- по символическому имени (см. errcode.h):
box.error(box.error.NO_SUCH_USER, 'guest')
-- обернуть нижнюю ошибку в свою (3.1+):
local ok, low = pcall(box.space.users.insert, box.space.users, {1})
if not ok then
box.error({ type = 'ServiceError',
reason = 'cannot register user',
prev = low })
end
Код: Выделить всё
box.error.is(err) -- 3.2+: true, если это объект ошибки
-- xpcall ловит И запускает обработчик ДО разворачивания стека:
local ok, res = xpcall(work, function(e)
return debug.traceback(tostring(e), 2)
end)
Код: Выделить всё
команда назначение
--------------------- -------------------------------------------
pcall(f, ...) поймать, вернуть ok + значение/ошибку
xpcall(f, handler,...) поймать + обработчик до сворачивания стека
box.error.new(...) создать объект (не бросает)
box.error(...) создать и/или бросить
err:raise() бросить готовый объект
box.error.last() последняя ошибка файбера
box.error.clear() очистить слот last
box.error.set(err) записать last вручную
err:unpack() таблица из code/type/message/trace
box.error.is(x) это объект ошибки? (3.2+)
box.error.cfg{...} traceback_enable on/off
Главная ловушка: значение из pcall - это объект (cdata), а не строка. Конкатенация ('msg: ' .. err) сработает через __tostring, но сравнения err == 'some text' или err:sub(...) сломаются. Всегда обращайтесь к err.message / err.code / err.type.
- error('строка') теряет код и тип - получится обычная ClientError без полезной семантики. Для бизнес-ошибок используйте box.error.new с code и type.
- xpcall: обработчик выполняется ДО разворачивания стека, поэтому именно в нём (а не после) имеет смысл звать debug.traceback. Если обработчик сам бросит ошибку - получите запутанный двойной сбой, держите его коротким.
- Трейсбек по сети не приезжает: после net.box вызова err.trace отражает серверную C-сторону, а Lua-стек удалённого файбера недоступен. Для распределённой диагностики кладите контекст в message или в prev-цепочку.
- box.error.last() - per-fiber. Не рассчитывайте увидеть в нём ошибку из другого файбера; и помните, что успешный вызов её не очищает - читайте last сразу после сбоя.
- traceback_enable стоит денег: на горячем пути с массой ожидаемых ошибок его иногда выключают ради latency.
- Транзакции: пойманная через pcall ошибка НЕ откатывает транзакцию автоматически. После box.begin() ловите ошибку и сами решайте box.commit() или box.rollback().
- Создайте спейс users с первичным ключом. Вставьте кортеж {1, 'Ann'}, затем во втором pcall вставьте {1, 'Bob'}. Поймайте ошибку и напечатайте отдельно err.code, err.type, err.message. Затем оберните её в свою ошибку через box.error.new({type='UserDupError', reason='id taken', prev=err}) внутри pcall, поймайте уже её и пройдите по полю .prev, распечатав цепочку причин сверху вниз. Сравните вывод box.error.last() сразу после сбоя.
- Что физически возвращает pcall во втором значении при ошибке Tarantool - строку или объект, и какие у него ключевые поля?
- Чем box.error.new отличается от box.error при одинаковых аргументах?
- Почему после net.box-вызова вы не увидите полный Lua-трейсбек удалённой стороны и где тогда искать контекст?
- Зачем нужны set_prev/prev и чем error chaining помогает диагностике в многослойном коде?