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


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.


Errors with range over func

Last week I introduced the topic of range-over-func as an iterator pattern. Let’s look at how error handling differs with this approach. For context, let’s compare the custom iterator approach, which has three distinct errors to check: 1. The initial function call, 2. while iterating over each row, 3. finally at the end, to ensure iteration completed successfully. orders, err := db.Orders(ctx, userID) if err != nil { return err } defer orders.


Range over func

Since Go 1.23, we’ve had a new way we can implement iterators. I’ve written previously about it if you’re interested. But today we’ll take a glance at how it affects our Orders example: orders, err := db.Orders(ctx, userID) if err != nil { return err } for order, err := range orders { if err != nil { return err } /* Do something with each order */ } Unless you’re already quite familiar with the range-over-func feature, it’s probably not immediately clear what’s going on here, just by looking at the code.

Get daily content like this in your inbox!

Subscribe