[Lua] Yielding during a function ref breaks error handling

CreateThread(function()
    exports.test:cb(function()
        print(1)
        Wait(500)
        print(2)
        print(undefined.value)
        print(3)
    end)
end)

Modifying Citizen.Await to print the error will show the message.

	if promise.state == 2 then
		print(promise.value)
		error(promise.value)
	end


This isn’t a new bug and does occur from prior to “tickless” resources, but no error was previously thrown and the coroutine would stay suspended.
image



If the export being called originates from another runtime (or, at least JS) then no error will be thrown; instead the console will print an empty line.

exports('promise', () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('promise');
        }, 500);
    });
})

exports('cb', (cb) => {
    cb('cb');
})
CreateThread(function()
    exports.test:cb(function()
        print(1)
        exports.test:promise()
        print(2)
        print(undefined.value)
        print(3)
    end)
end)

What would more logical behavior be here? Capturing a stack when yielding no matter what would be weird and inefficient - same would go for storing a stack capture when encountering an await-style callback.

I’m not sure if await-style callbacks support errors - that could be more suited (and that implies this is just a case of stack boundaries not being added?) for this setup.

An error and stack trace is created from xpcall and displays if you print normally, but not when passed to error. I was also able to use .catch in JS and print to console.

I personally use promises over callbacks and never have issues; but people have been reporting this as an issue with oxmysql when going from callback API into a promise.

The ‘repros’ you provided seem a bit incomplete (I’m not seeing any Lua implementation of cb for example) so I can’t really say what led to which behavior, but it is to be noted that in the following case the Lua error does print correctly (as a yielding export or function call will map to a promise in JS - for a more universal example you can check if it is a promise and await if so):

exports('cb', async (cb) => {
    try {
        await cb('cb');
    } catch (e) {
        console.log('failure from cb', e);
    }
})
RegisterCommand('trip', function()
    CreateThread(function()
        exports[GetCurrentResourceName()]:cb(function()
            print(1)
            Wait(500)
            print(2)
            print(undefined.value)
            print(3)
        end)
    end)
end)

Unhandled promise failures don’t print anything at all (anymore?) but I believe this was a much more problematic issue last time this was looked into as sometimes it may look unhandled from the JS size but actually be handled fine elsewhere (I think actually in the case where cb is correctly marked as an async function this should propagate to a Lua error in the caller! the case where it’s not async/await silently failing however is another weird edge case).

For now I’m scoping the investigation to just figuring out why the error gets silently swallowed in both cases, with the note that await-ing is the recommended thing to do as unhandled promise rejections are odd in any case.

Just a simple one-liner to create an export and trigger the callback.

exports('cb', function(cb) cb() end)

I tried modifying the return for doStackFormat when “recovering from an error”, but I wouldn’t assume it’s so simple (side effects?).

local function doStackFormat(err)
	local fst = FormatStackTrace()
	
	-- already recovering from an error
	if not fst then
		-- return nil
		return err
	end

	return '^1SCRIPT ERROR: ' .. err .. "^7\n" .. fst
end

Pushed a somewhat-helping changeset for some scenarios here (and other error reporting fixes) - hasn’t been fully tested to not change/provide too much info other cases but I’d prefer providing excessive info to no info whatsoever.

Repros

Resource

fxmanifest.lua

fx_version 'cerulean'
game 'common'

server_scripts {
    'test.js',
    'test.lua',
}

test.js

exports('promise', () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('promise');
        }, 500);
    });
})

exports('cb', async (cb) => {
    const rv = cb('cb');

    if (rv instanceof Promise) {
        await rv;
    }
});

exports('cbn', (cb) => {
    cb('cb');
});

test.lua

exports('cb_samert', function(cb)
    cb()
end)

RegisterCommand('trip', function()
    CreateThread(function()
        print('a')

        exports[GetCurrentResourceName()]:cb(function()
            print(1)
            Wait(500)
            print(2)
            print(undefined.value)
            print(3)
        end)

        print('b')
    end)
end)


RegisterCommand('tripn', function()
    CreateThread(function()
        print('a')

        exports[GetCurrentResourceName()]:cbn(function()
            print(1)
            Wait(500)
            print(2)
            print(undefined.value)
            print(3)
        end)

        print('b')
    end)
end)

RegisterCommand('trip_promise', function()
    CreateThread(function()
        exports[GetCurrentResourceName()]:cb(function()
            print(1)
            exports[GetCurrentResourceName()]:promise()
            print(2)
            print(undefined.value)
            print(3)
        end)
    end)
end)

RegisterCommand('trip_lua', function()
    CreateThread(function()
        exports[GetCurrentResourceName()]:cb_samert(function()
            print(1)
            Wait(500)
            print(2)
            print(undefined.value)
            print(3)
        end)
    end)
end)

Example output

trip

[script:error_test_48] SCRIPT ERROR: @error_test_4871908/test.lua:9:
[script:error_test_48]  An error occurred while calling export `cb` in resource `error_test_4871908`:
[script:error_test_48]   citizen:/scripting/lua/scheduler.lua:747: SCRIPT ERROR: @error_test_4871908/test.lua:13: attempt to index a nil value (global 'undefined')
[script:error_test_48]   > ref (@error_test_4871908/test.lua:13)
[script:error_test_48]  ---
[script:error_test_48] > fn (@error_test_4871908/test.lua:9)

tripn

This is arguably the ‘worst’ case here but that’s because code is kind of wrong.

[script:error_test_48] SCRIPT ERROR in promise (unhandled rejection): Error: SCRIPT ERROR: @error_test_4871908/test.lua:30: attempt to index a nil value (global 'undefined')
[script:error_test_48] > ref (@error_test_4871908/test.lua:30)
[script:error_test_48]
[script:error_test_48]

trip_promise

[script:error_test_48] SCRIPT ERROR: @error_test_4871908/test.lua:40:
[script:error_test_48]  An error occurred while calling export `cb` in resource `error_test_4871908`:
[script:error_test_48]   citizen:/scripting/lua/scheduler.lua:747: SCRIPT ERROR: @error_test_4871908/test.lua:44: attempt to index a nil value (global 'undefined')
[script:error_test_48]   > ref (@error_test_4871908/test.lua:44)
[script:error_test_48]   > process.runNextTicks [as _tickCallback] (node:internal/process/task_queues:61)
[script:error_test_48]  ---
[script:error_test_48] > fn (@error_test_4871908/test.lua:40)
[script:error_test_48] > process.runNextTicks [as _tickCallback] (node:internal/process/task_queues:61)

trip_lua

[script:error_test_48] SCRIPT ERROR: @error_test_4871908/test.lua:52:
[script:error_test_48]  An error occurred while calling export `cb_samert` in resource `error_test_4871908`:
[script:error_test_48]   citizen:/scripting/lua/scheduler.lua:747: SCRIPT ERROR: citizen:/scripting/lua/scheduler.lua:747: SCRIPT ERROR: @error_test_4871908/test.lua:56: attempt to index a nil value (global 'undefined')
[script:error_test_48]   > ref (@error_test_4871908/test.lua:56)
[script:error_test_48]
[script:error_test_48]   > ref (@error_test_4871908/test.lua:2)
[script:error_test_48]  ---
[script:error_test_48] > fn (@error_test_4871908/test.lua:52)
1 Like