vimtricks.wiki Concise Vim tricks, one at a time.

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 of fn that always runs scheduled — useful for passing to callbacks
  • vim.defer_fn(fn, timeout_ms) is similar but introduces an intentional time delay; use vim.schedule() when you need 0-delay deferred execution
  • Any call to vim.api.*, vim.cmd(), vim.notify(), or buffer/window functions needs vim.schedule() if called from a non-main-loop context

Next

How do I programmatically read and write Vim register contents including their type from Vimscript?