This should be a quick reference guide for those familiar with NodeJS on how to execute the same async tasks in Lua using luv. This is aimed towards use cases inside Neovim, but is not limited to those cases.


    We want to spawn a child task to convert a markdown document into HTML using pandoc. This could be used as part of a publishing flow for a blog, for writing notes, or for implementing a markdown previewer.

    // in Node we could do it like this:
    const {spawn} = require('child_process');
    const [sourceFile, destinationFile] = process.argv.slice(2)
    const convert = spawn('pandoc', [sourceFile, '--from', 'gfm', '--to', 'html5', '-o', destinationFile, '-s', '--highlight-style', 'tango'])
    convert.stderr.on('data', (data) => {
    convert.on('close', (code) => {
      console.log(`child process exited with code ${code}`);

    In Lua, the code looks very similar, but a bit more verbose:

    -- The same as before, but this time we want to generate the file names 
    -- based on the file we are currently editing instead of passing them as commandline args
    function convert()
      -- cut off the `.md` part of the file
      local destinationFile = vim.fn.expand('%:t:r')
      -- the name of the file you're editing
      local sourceFile = api.nvim_buf_get_name(0)
      spawn('pandoc', {
        args = {sourceFile, '--from', 'gfm', '--to', 'html5', '-o', string.format('%s.html', destinationFile), '-s', '--highlight-style', 'tango'},
      {stdout = function()end, stderr = function(data) print(data) end},
      function(code) -- we want to call this function when the process is done
        print('child process exited with code ' .. string.format('%d', code))
    function spawn(cmd, opts, input, onexit)
      local handle, pid
      -- open an new pipe for stdout
      local stdout = vim.loop.new_pipe(false)
      -- open an new pipe for stderr
      local stderr = vim.loop.new_pipe(false)
      handle, pid = vim.loop.spawn(cmd, vim.tbl_extend("force", opts, {stdio = {stdout; stderr;}}), 
      function(code, signal)
        -- call the exit callback with the code and signal
        onexit(code, signal)
        -- stop reading data to stdout
        -- stop reading data to stderr
        -- safely shutdown child process
        -- safely shutdown stdout pipe
        -- safely shutdown stderr pipe
      -- read child process output to stdout
      vim.loop.read_start(stdout, input.stdout)
      -- read child process output to stderr
      vim.loop.read_start(stderr, input.stderr)
    function safe_close(handle)
      if not vim.loop.is_closing(handle) then

    One of the major differences is that in Lua you are responsible for cleaning up both the process handle and any pipes you have open to receive data from that handle. It then becomes useful to create a more generalized spawn function to handle all of these things under the hood, allowing you to just call spawn in a similar manner to the NodeJS API.

    See Also

    Mentioned around the web