Design document for improving error handling in the Lua language in the context of running on a MediaWiki-based wiki.


Whenever we run into a runtime error, the #invoke call will return a stack trace of the error in question (through debug.traceback). Sometimes the stack trace is not sufficient to trace down an error or if one thing crashes within a function call, an entire portion (or the whole article in the case of individual Void Relic pages) of an article ceases to exist. This is problematic on articles that rely on Lua modules for generating wikitext content since readers cannot access article content. To access the content, they will have to:

  • wait until a community developer fixes the error
  • wait until a temporary fix is applied
  • access the content outside of the wiki (e.g. third-party sites and blogs)
  • use an archived version of the wiki article

A single minute error can prevent access to valuable information that was generated error-free. This "hail mary" or all-or-nothing approach makes our codebase vulnerable to new changes as WARFRAME releases new updates.

Proposed Solutions[]

Function Calls With Soft Error Handling[]

Since functions are first class variables in Lua, we can change the calling behavior through the use of metatables. Updating the __call metamethod of each module's export table to catch runtime errors and return a dummy value (that matches the intended return type so that parent call won't crash out) and the error message while adding "[[Category:Pages with script errors]]" to the frame context that invokes the function.

We can update Module:Lua or add a new module that other modules can import to inherit this behavior through OOP.

Wrapping Function Calls To Catch Errors[]

Alternatively, we can use a wrapper function that performs soft error handling like protected calls pcall() or xpcall. These are already built into the Scribunto core library. The problem is that not every function should have a protected call since it hides errors from the developers compared to an error() call.

Solutions to Avoid[]

Returning Error Strings[]

Errors should not be in the form of strings because the caller function cannot differentiate between an error and a valid string value without additional code overhead in the caller function.

Example from Module:Tooltips:

---	Frame preprocessor.
--	@function		pp
--	@param			{function} func
--	@return			{function} tooltip span applied to return value of func
local function pp(func)
	return function(...)
		local frame = type(...) == 'table' and (...).args and ... or { args = {...} }
		local name, source, index = (function(t) return t[1], t[2], t[3] end)(frame.args)
		if type(name) == 'table' then
			index,name = name,index
			source = index[1]
			name = index.name or index[2]
		if type(index) ~= 'table' then
			local error = '<strong class="error scribunto-error" title="%s">%s</strong>[[Category:Pages with script errors]]'
			if source == '' or not source then
				return error:format(debug.traceback('pp(func): source is '..type(source)..'. ', 2):gsub('[<>]', ''),
					'Where is "'..(name or '<nil>')..'"?')
			elseif name == '' or not name then
				return error:format(debug.traceback('pp(func): name is '..type(name)..'. ', 2):gsub('[<>]', ''),
					'What did you want in "'..(source or '<nil>')..'"?')
			name = mw.text.decode(name)
			index = makeData(source, frame) or require('Module:Tooltips/icon')[source](name)
			if not index then
				return error:format(debug.traceback('pp(func)', 1):gsub('[<>]', ''), 'Where is "'..name..'"? Wasn\'t found in '..source)
		elseif not index[2] then
			--index is a data entry
			index = require('Module:Tooltips/icon')[source](index,name)
		source = index[1]
		-- Damage tooltips are unique in that they both need invert and hue-rotate filters for colored damage icons and text
		return ('<span class="tooltip tooltip-full %s" data-param="%s" data-param2="%s">%s</span>')
			:format(source == 'DamageTypes' and 'damage-type-tooltip' or '', 
				index[2] or mw.log('pp(func): no index[2]:', name, index[1]) or name or index[3], source or error('no sauce :('..mw.dumpObject(index)) or 'nil', 
				func(name, index, frame.args)