How do I safely run Neovim API calls from inside a fast callback or loop without causing errors?
Answer
vim.schedule()
Explanation
vim.schedule() defers a Lua function to run on the main Neovim event loop after the current fast-event context completes. Calling Neovim API functions directly from certain callbacks (like vim.loop libuv callbacks, on_stdout handlers, or vim.fn.jobstart callbacks) will error because Neovim is not in a state where UI changes are safe. Wrapping those calls in vim.schedule() solves this.
How it works
vim.schedule(function()
-- Safe to call vim.api, vim.cmd, vim.notify, etc.
end)
The callback is placed in a queue and executed on the next safe iteration of the event loop — after the current fast callback returns. This is similar to setTimeout(..., 0) in JavaScript.
Example
Without vim.schedule(), this would crash from inside an async job callback:
-- WRONG: calling vim.api from inside a libuv timer callback
local timer = vim.loop.new_timer()
timer:start(500, 0, function()
vim.notify("Done!") -- Error: fast-event context
end)
-- CORRECT: defer to main loop
timer:start(500, 0, function()
vim.schedule(function()
vim.notify("Done!") -- Safe
end)
end)
Tips
- Use
vim.schedule_wrap(fn)as a shorthand to create a wrapped version offnthat always runs scheduled — useful for passing to callbacks vim.defer_fn(fn, timeout_ms)is similar but introduces an intentional time delay; usevim.schedule()when you need 0-delay deferred execution- Any call to
vim.api.*,vim.cmd(),vim.notify(), or buffer/window functions needsvim.schedule()if called from a non-main-loop context