A context-aware snippet for Go error handling code, returning the right types, with the default values.


Golang’s error handling is notoriously verbose. It was also the top pain point in the Go Developer Survey Q2 2022. Numerous proposals to simplify error handling have been written, but at the time of writing, none have been accepted.

So we have to live with the current state of things. It’s ok, Neovim with Treesitter has a good “understanding” of code, so we can use it to generate the error handling code.


One of Neovim 0.5 most exciting features was the introduction of Treesitter. This new parsing system gives the editor a basic understanding of the code at hand: the editor “knows” what the function name is, where a variable is defined… It enables great plugins, showing the context around some code, improved selection, better renaming and navigation and more! In this post, we will look into how Treesitter can be combined with LuaSnip for smarter snippets.

Error Handling in Go

Feel free to skip this section if you already know about Go error handling.

In Golang, errors are handled by returning a value of type error, sometimes along the types returned when everything goes well. For instance, the time.Parse function has the following signature:

func Parse(layout, value string) (Time, error)

time.Parse will try to parse a string with the given format. If it’s successful, it’ll return a value of type Time and the error will be nil. If a date can’t be parsed, then the error won’t be nil. And so it’s common in Go to check for errors like so:

timeStr := "Aug 6, 2022 @ 7:54pm"
t, err := time.Parse("Jan 2, 2006 @ 3:04pm", timeStr)
if err != nil {
// Now, we can safely use t

In the example above, we just log the error and panic. But if we are in a function, we will likely return an error to the caller, wrapping the error returned by time.Parse. For instance:

1func parseLong(timeStr string) (Time, error) {
2    t, err := time.Parse("Jan 2, 2006 @ 3:04pm", timeStr)
3    if err != nil {
4        return t, fmt.Errorf("couldn’t parse %v: %v", timeStr, err)
5    }
6    return t, nil

The key takeaway here is that the return line in the error case (line 4 above) depends on the return type of the function and so can vary significantly.

This is covered in more details in this post on the Go blog.

Clever Snippets with LuaSnip and Treesitter

As we have seen, error handling code varies depending on the number and type of the values returned by the parent function. So we are very often writing variations of the following in our Go code:

func f(arg1 string, arg2 string) (…, error) {
    val, err := someFunction(arg1, arg2)
    if err != nil {
        return …, err

Or with 3 return values:

func f(arg string) (…, …, error) {
    val, err := someFunction(arg)
    if err != nil {
        return …, …, err

It would be handy to have a snippet that would adjust the return’s shape depending on the return type of the function that contains it.


Here is a short asciicast demonstration1 of the snippet we are going to build. It is called smart_err:

The snippet inserted (line 11 in the asciicast) is

myVal, myErr := anotherFunc(arg1, arg2)
if myErr != nil {
	return false, fmt.Errorf("anotherFunc: %v", myErr)

when the return type of the function was (bool, error).


val, err := f()
if err != nil {
	return false, fmt.Errorf("f: %v", err), 0, ""

when the return type was (bool, error, int, string).

And it even works in nested functions:

func myFunc() (*MyStruct, error) {
    // ...
	a := func() (bool, error, int, string) {
		// The snippet detected that we need 4 values here,
		// and not 2 as in the outer function
		return v, nil, 0, ""

Also note that we can choose to wrap the containing error, or not, with a keystroke (bound to require("luasnip").change_choice(1)). The snippet switches between the following 3 forms (line 18 in the asciicast):

  1. return false, err
  2. return false, fmt.Errorf("f: %v", err)
  3. return false, errors.Wrap(err, "f")

That’s pretty cool! Let’s see how to add it to your Neovim configuration.


The code to define this snippet was initially written by TJ DeVries2. I’ve only slightly adapted it so that it fits in one file. I believe the author permits this, as the readme states3:

This is TJ’s configuration repo. Feel free to use whatever you would like from it! It’d be great if you mentioned where it came from if you think it’s cool.

Copy this alongside your other LuaSnip snippets, for instance in ~/.config/nvim/snippets/go.lua:

  1local ls = require "luasnip"
  2local f = ls.function_node
  3local s = ls.s
  4local i = ls.insert_node
  5local t = ls.text_node
  6local d = ls.dynamic_node
  7local c = ls.choice_node
  8local snippet_from_nodes =
 10local ts_locals = require "nvim-treesitter.locals"
 11local ts_utils = require "nvim-treesitter.ts_utils"
 12local get_node_text = vim.treesitter.get_node_text
 14-- Adapted from
 15local function same(index)
 16  return f(function(args)
 17    return args[1]
 18  end, { index })
 21-- Adapted from
 23  "go",
 24  "LuaSnip_Result",
 25  [[ [
 26    (method_declaration result: (_) @id)
 27    (function_declaration result: (_) @id)
 28    (func_literal result: (_) @id)
 29  ] ]]
 32-- Adapted from
 33local transform = function(text, info)
 34  if text == "int" then
 35    return t "0"
 36  elseif text == "error" then
 37    if info then
 38      info.index = info.index + 1
 40      return c(info.index, {
 41        t(string.format('fmt.Errorf("%s: %%v", %s)', info.func_name, info.err_name)),
 42        t(info.err_name),
 43        -- Be cautious with wrapping, it makes the error part of the API of the
 44        -- function, see
 45        t(string.format('fmt.Errorf("%s: %%w", %s)', info.func_name, info.err_name)),
 46        -- Old style (pre 1.13, see, using
 47        --
 48        t(string.format('errors.Wrap(%s, "%s")', info.err_name, info.func_name)),
 49      })
 50    else
 51      return t "err"
 52    end
 53  elseif text == "bool" then
 54    return t "false"
 55  elseif text == "string" then
 56    return t '""'
 57  elseif string.find(text, "*", 1, true) then
 58    return t "nil"
 59  end
 61  return t(text)
 64local handlers = {
 65  ["parameter_list"] = function(node, info)
 66    local result = {}
 68    local count = node:named_child_count()
 69    for idx = 0, count - 1 do
 70      table.insert(result, transform(get_node_text(node:named_child(idx), 0), info))
 71      if idx ~= count - 1 then
 72        table.insert(result, t { ", " })
 73      end
 74    end
 76    return result
 77  end,
 79  ["type_identifier"] = function(node, info)
 80    local text = get_node_text(node, 0)
 81    return { transform(text, info) }
 82  end,
 85-- Adapted from
 86local function go_result_type(info)
 87  local cursor_node = ts_utils.get_node_at_cursor()
 88  local scope = ts_locals.get_scope_tree(cursor_node, 0)
 90  local function_node
 91  for _, v in ipairs(scope) do
 92    if
 93      v:type() == "function_declaration"
 94      or v:type() == "method_declaration"
 95      or v:type() == "func_literal"
 96    then
 97      function_node = v
 98      break
 99    end
100  end
102  local query = vim.treesitter.query.get("go", "LuaSnip_Result")
103  for _, node in query:iter_captures(function_node, 0) do
104    if handlers[node:type()] then
105      return handlers[node:type()](node, info)
106    end
107  end
109  return { t "nil" }
112-- Adapted from
113local go_ret_vals = function(args)
114  return snippet_from_nodes(
115    nil,
116    go_result_type {
117      index = 0,
118      err_name = args[1][1],
119      func_name = args[2][1],
120    }
121  )
124return {
125  -- Adapted from
126  s("smart_err", {
127    i(1, { "val" }),
128    t ", ",
129    i(2, { "err" }),
130    t " := ",
131    i(3, { "f" }),
132    t "(",
133    i(4),
134    t ")",
135    t { "", "if " },
136    same(2),
137    t { " != nil {", "\treturn " },
138    d(5, go_ret_vals, { 2, 3 }),
139    t { "", "}" },
140    i(0),
141  }),

We start by importing and aliasing some LuaSnip functions. The snippet itself is defined at the end (line 126). Most of those functions were covered in the previous post, see there for calls to s, i and t. The clever part of this snippet is the dynamic node d on line 138. There, we call a function that queries (line 22 and 102) Treesitter to find the return types of the current function in Go code. Depending on the types found, default values for the type are used (line 33).

Treesitter Query

The Treesitter query is where the magic happens, so let’s take a deeper look.

Treesitter parses code and builds a tree with node for various parts of the code. Each node can have children, like a function definition has a name, a body… This tree can be queried in a Lisp-like language (s-expressions). For instance, in our snippet definition, we use this query:

2    (method_declaration result: (_) @id)
3    (function_declaration result: (_) @id)
4    (func_literal result: (_) @id)

The bracket syntax returns the nodes that match any of the patterns inside the brackets. So it’s effectively the union of all the nodes matched by any of the 3 patterns (on lines 2 to 4). Let’s take a concrete example and put the Go code in the Treesitter playground, with a trimmed down version of the query:

The Treesitter playground with the example Go code above inserted at the top, the query in the middle and the tree at the bottom

The tree built by Treesitter is at the bottom, with the corresponding code at the top and the query in the middle. The function_declaration node is highlighted in gray in the tree, like the corresponding part of the code (from line 13 to line 22). This node has a number of children, with the function name, the parameters of the function, a result node for the return type and body for the body of the function between curly braces.

Taking the first pattern on the playground, slightly modified:

    (function_declaration result: (_) @id2)

The part it captures is highlighted in blue in the Go code: (*MyStruct, error). It makes sense, since the function_declaration (the part on the gray background) has a result child, that’s captured by @id.

Now, on to the second pattern:

    (func_literal result: (_) @id3)

Similarly, further down in the body of that function, the return type (bool, error, int, string) of the anonymous function is matched.


I started writing this post over a year ago. The snippet has been very stable and kept working as without changes, which is a testament to the stability of the LuaSnip (and Treesitter) APIs. I’m still using it when I write Go code, even if some corner cases (like custom structs) aren’t handled. The snippet is quite complicated and niche, so I’m not sure that I would have invested the time to write it myself. I probably don’t write enough Go code to justify the investment, but finding it shared by someone buried in their config file made it worth it. I hope you have found this port useful, either to copy this particular snippet or as an example of advanced snippets.

This is the last post in a series on LuaSnip.


If you want to dig deeper:

  1. The test code is available as gist↩︎

  2. By the way, he has a very interesting Twitch channel ↩︎

  3. At the time of writing. See and the wayback machine for a snapshot. ↩︎