Neovim gains native concurrency library with vim.async
The new structured concurrency API at the editor's core promises to put an end to nested callback hell and to conflicts between third-party coroutine libraries.

O Neovim now includes a native structured concurrency library in its standard Lua library, under the vim.async namespace. The idea is to offer a standardized way to orchestrate asynchronous flows without blocking the editor's main event loop, taking plugin authors out of the fragmented territory of callbacks and improvised coroutine wrappers.
According to the official lua-async documentation, the model is inspired by the principles of structured concurrency, the same concept that has gained traction in modern runtimes such as Kotlin, Swift, and Python's asyncio. The feature has already been merged, and the community's reception on r/neovim was, according to InfoQ, largely celebratory.
What existed before (and why it hurt)
Historically, plugins that handle filesystem operations, background processes, and network calls relied on one of the following options:
- Libuv's event loop bindings, exposed via
vim.uv(formerlyvim.loop); - External libraries such as plenary.nvim and async.nvim.
The problem is familiar to anyone who has maintained a plugin of any size: raw Libuv callbacks follow the error-first pattern and, when chained, turn into that pyramid of indentation that's hard to read and even harder to debug. Third-party libraries, in turn, each solved the problem their own way, with divergent coroutine implementations. The practical result was dependency collision: two plugins pulling in competing async libraries could clash within the same configuration.
The arrival of vim.async responds to an initiative the project has been tracking for some time: establishing standard concurrency primitives directly in the editor's core, addressing task lifecycle, cancellation propagation, and error containment.
How the model works
In the new design, asynchronous routines run inside Tasks, created via vim.async.run(). Scheduling is strictly cooperative, built on stackful coroutines. When a task waits for an event or I/O operation with vim.async.await(), Neovim suspends that execution frame and hands control back to the event loop. In practice, this ensures that the editor's synchronous operations and user input keep running without freezing while the heavy work happens in the background.
The central point of structured concurrency is the explicit hierarchy between tasks:
- Every child task started within an existing task automatically attaches to its parent's concurrency scope;
- A parent task does not resolve until all of its attached children have finished;
- An unhandled exception in a child task propagates immediately to the parent, triggering cancellation among sibling tasks, unless they are isolated.
When you need a background process to outlive the task that started it, Task:detach() promotes that work to an independent top-level task. It's the escape valve for the scenario where the cancellation tree shouldn't reach that process.
The primitives that come in the box
For synchronization and flow control, vim.async brings a set of primitives modeled on current concurrency runtimes:
| Primitive | What it's for | |---|---| | vim.async.semaphore() | Restricts the number of concurrent permits in parallel executions | | vim.async.timeout() | Applies hard cancellation deadlines | | vim.async.iter() | Consumes task results in completion order, not launch order | | vim.async.pawait() | Async analogue to Lua's pcall(): returns a status alongside the result or error payload |
vim.async.pawait() deserves attention from anyone who has been burned by errors in asynchronous callbacks. In the technical discussions cited by InfoQ, the community clarified exactly this difference in behavior: vim.async.await() handles Libuv-style error-first callbacks one way, while pawait() works for operations that can fail in an expected way without invalidating the caller, delivering the status-result pair instead of throwing.
There's also a bridge to synchronous code. Methods like Task:wait() and Task:pwait() pump the event loop until completion, allowing a synchronous section of Neovim to wait for an asynchronous task to finish. It's what you use when you need an asynchronous result in the middle of a function that can't be suspended.
What changes for plugin authors
If you maintain or plan to write a Neovim plugin that does heavy I/O, the message is direct: there's now an official concurrency foundation to bet on, without loading plenary.nvim just for its async module or reinventing a coroutine wrapper. This reduces an entire class of bugs, those from manually managing Libuv callbacks, which the community itself described as fragile and error-prone.
The stability gain comes from the design: with the parent-child hierarchy, cancellation and error propagation stop being something each author implements (or forgets to implement) by hand. An operation that fails brings down the scope predictably, instead of leaving orphan tasks running or silently swallowing exceptions, one of the causes of crashes in concurrent operations.
One note of sobriety is worth adding: for now, this is new API in the core. Migrating existing plugins that already depend on plenary or async.nvim isn't automatic, and the two approaches will likely coexist in the ecosystem for a good while. It also remains to be seen how quickly third-party libraries will adopt (or start wrapping) vim.async as a common foundation, which is what actually eliminates dependency collisions.
To start experimenting, the way forward is to read the lua-async documentation, which provides full API reference and usage examples, and to see how run, await, and pawait combine in a real flow before migrating production code.
Translated from the Brazilian Portuguese original · Read the original
Perplexity swaps DynamoDB for in-house database and cuts latency by 5x
The company behind the AI-powered search engine migrated its serving layer to CobbleDB, an internal database written in Rust, and cut batch read latency by up to 5x while saving at least 20% on storage.