Alternatives to iter.Seq3

November 18, 2025

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:

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

Notice we once again have omitted the check for scanner.Err()—we’ll get back to that next. But first, just to illustrate how our caller now works:

result, err := grep(r, pattern)
if err != nil {
  return err
}
for lineNo, line := range result {
  fmt.Printf("Line %d matches: %s\n", lineNo, line)
}

So… how do we get our error handling back?

I know of three options:

  1. Combine our results into a single struct, so that we can use the second value of iter.Seq2 for errors again. e.g.:

    type Match struct {
      LineNumber int64
      Line       string
    }
    
    func grep(r io.Reader, pattern string) iter.Seq2[Result, error] {
    
  2. Arrange for an Err() method to be called after the iterator has exited. This can be clumsy, but in some cases—usually complicated ones, where you have special requirements for extra methods—it’s the best option. but one approach would be to make grep return a Result object with an Iter() method and a Err() method.

    type Matches struct { /* private fields */ }
    
    // Iter returns an iterator.
    func (m *Matches) Iter() iter.Seq2[int64, string]
    
    // Err returns a error, if one occurred, during iteration. Should be called
    // after Iter() is consumed.
    func (m *Matches) Err() error
    

    (I’m omitting the implementation of this one, because it should probably very rarely be used. But if you’d like to see how to implement it, hit reply and let me know. I can dedicate an email to that topic. 😊)

  3. Use a callback.

    func grep(r io.Reader, pattern string, final func(error)) (iter.Seq2[int64, string], error) {
    

    In this case, errDB is called after the iterator completes, with whatever value is returned by scanner.Err() (possibly nil):

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

Which approach should you use? Well, it’s hard to offer a blanket rule, because there are many different scenarios. But personally, I tend to reach for the Result struct pattern first. I would very rarely use the Err() method approach, just because of the added overhead it requires in setting up new types. I do use the callback approach occasionally, but usually when there’s other metadata to return (like pagination data), not just for an error.

Which pattern do you like best? Are there any I’ve overlooked?


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:


Handling errors during iteration with range-over-func

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.


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