Handling errors during iteration with range-over-func

November 13, 2025

Yesterday we looked at a range-over-func iterator that was missing a vital piece: Error handling during iteration.

I know of three possible solutions to this problem, and today we’ll look at the simplest of them, which I typically recommend: Using iter.Seq2.

We’ve already looked at this pattern from the consumer’s perspective. Today we’ll see how the implementation works.

func grep(r io.Reader, pattern string) (iter.Seq2[string, error], error) {
  re, err := regexp.Compile(pattern)
  if err != nil {
    return nil, err
  }
  scanner := bufio.NewScanner(r)
  return func(yield func(string) bool) {
    for scanner.Scan() {
      line := scanner.Text()
      if re.MatchString(line) {
        if !yield(line, nil) {
          return
        }
      }
    }
    if err := scanner.Err(); err != nil {
      yield("", err)
    }
  }, nil
}

This version differs from the previous in just two small ways:

  1. Rather than returning iter.Seq[string] the function returns iter.Seq2[string, error].
  2. We check scanner.Err() after the scanner.Scan() loop terminates, and may yield one final value ("", err), if we encounter an error.

From the caller’s standpoint, the code changes from this:

matches, err := range grep(r, "pattern")
if err != nil {
  return err
}
for match := range matches {
  /* do something with the matches */
}

to:

matches, err := range grep(r, "pattern")
if err != nil {
  return err
}
for match, err := range matches {
  if err != nil {
    return err
  }
  /* do something with the matches */
}

Not a big change, and generally much more natural than the alternatives, which we’ll consider in the upcoming days.

The two biggest drawbacks to this approach, in my opinion, are:

  1. Redundant error handling (but we can improve on that—see below)
  2. As discussed earlier, in the context of consuming iterators, it’s ambiguous, from the function signature, whether or not iteration errors are terminal. But if the caller assumes they are not terminal, things will still work.

To address the redundant error handling, can improve slightly on the calling semantics, by handling the possible error from regexp.Compile in the iterator. With this change, we’re left with this implementation:

func grep(r io.Reader, pattern string) iter.Seq2[string, error] {
  return func(yield func(string) bool) {
    re, err := regexp.Compile(pattern)
    if err != nil {
      yield("", err)
      return
    }
    scanner := bufio.NewScanner(r)
    for scanner.Scan() {
      line := scanner.Text()
      if re.MatchString(line) {
        if !yield(line, nil) {
          return
        }
      }
    }
    if err := scanner.Err(); err != nil {
      yield("", err)
    }
  }, nil
}

and this caller:

for match, err := range range grep(r, "pattern") {
  if err != nil {
    return err
  }
  /* do something with the matches */
}

Share this

Direct to your inbox, daily. I respect your privacy .

Unsure? Browse the archive .

Related Content


Iterator callbacks

Today I want to expand with a thought I touched on yesterday: Callbacks with iterators. Yesterday’s context was error handling. But I’ve found callbacks with iterators to be very valuable in a slightly different case. To understand the problem callbacks have helped me solve, we need to move away from our grep example to something a bit more involved. Let’s imagine we’re querying a REST API, which returns paginated results. A typical response body might look something like this, in JSON:


Alternatives to iter.Seq3

Last time we modified our range-over-func iterator to return a iter.Seq2, so that it could include a possible error value for each iteration. But this isn’t the only way to handle errors with range-over-func. And in fact, in some cases, it may not even be possible! Suppose you’re ranging over a key/value pair, for example. There is no iter.Seq3 option to return three values per iteration. To illustrate, let’s update our grep to return the line number, and the matching line:


Implementing a range-over-func iterator

It’s finally time to look at how we implement a range-over-func iterator. I’d venture a guess this should be the go-to pattern for most iterators, unless or until you have special needs that it won’t address. And, of course, we’ll discuss some of those in the near future, as well. First, here’s how our grep implementation looks using range-over-func: func grep(r io.Reader, pattern string) (iter.Seq[string], error) { re, err := regexp.

Get daily content like this in your inbox!

Subscribe