Errors with range over func

October 15, 2025

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.Close() // Ensure that the iterator is cleaned up, even in case of early return
for orders.Next() {
	order, err := orders.Process()
	if err != nil {
		return err
	}
	/* Do something with each order */
}
if err := orders.Err(); err != nil {
	return err
}

With the range-over-func variant, with only two errors to check: 1. The initial function call, and 2. while iterating over each row.

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 */
}

This reduction in errors is possible because range-over-func makes it easy to return an iteration error in the loop. The same can actually be accomplished with a custom iterator, as well, by changing the signature of Next from Next() bool to Next() error. This would make it possible to detect iteration errors within the iteration loop. This approach has some trade-offs. Perhaps most noticably, it makes the code slightly more cumbersome:

for {
  err := orders.Next()
  if err != nil {
    /* .. handle error .. */
  }
  /* Do something with each order */
}

It does have the advantage of making it much less likely to forget to check the iterator error, though.

Whether using range-over-func or a custom iterator, consolidating the error handling has one potential, though in my experience rare, drawback, that being that it’s not obvious when the error occurred. If your application needs to know that an error occurred when processing a row, versus during iteration, that’s less obvious. Custom error types can address this, but it is a bit more work.

And with that in mind, let’s discuss going one step further, and reducing error checking to a single instance,with range-over-func:

for order, err := range db.Orders(ctx, userID) {
	if err != nil {
		return err
	}
	/* Do something with each order */
}

In this variant, there’s only a single place to check an error. If the function call itself fails, it would return a single value-error pair with a non-nil error.

This is, generally, my preferred way to build iterators now. There’s just one caveat: Some iterators can return multiple errors. Others can return only a single error, which aborts the operation. As a consumer of a range-over-func iterator, you need to be aware of which you’re dealing with.

Here’s an example: Suppose you’re querying the Spotify API (something I do frequently, actually) for a list of 10 artists. But Spotify doesn’t know about two of those 10 artists, because one is your cousin’s brand new band, and they haven’t been published on Spotify yet, and the other includes a typo. In this case, the iterator may return results 1-3, then a not-found error, then results 5-9, then another not-found error.

If you abort as soon as an error occurs, you’ll miss some of the legitimate results.

var errs []error
for artist, err := range spotify.Artists(ctx, userID) {
	if err != nil {
		errs = append(errs, err)
	}
	/* Do something with each artist */
}
if len(errs) > 0 {
  log.Error("failed to fetch some Spotify artists", slog.Any("error", errors.Join(errs...)))
}

If you’re unsure whether a range-over-func iterator may return multiple errors, you can just program defensively, and assume it always does. Even if it only returns a single error, then aborts, your code should still work properly.


Share this

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

Unsure? Browse the archive .

Related Content


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.


Iterator patterns

I’m going to change gears from the previous discussion about goroutines, to different ways of iterating. This can closely relate to goroutines, so we’ll bounce back and forth a bit, I suspect. Meanwhile, if you have any questions specifically about goroutines that I didn’t cover, send me an email. I’ll be happy to fill in those gaps! First off, before really diving into iterator patterns, let’s talk about why we might want iterators.


Custom iterators

Until recently, custom iterators were probably the most common way to iterate over a list of elements that might trigger an error. Several examples exist in the standard library. Perhaps the most well known would be the sql.Rows type, which provides (among others), the following methods: Next() bool — Advances to the next item Scan(...any) error — Processes the current item Err() error — Reports an iteration error Close() error — Closes iterator, possibly before the last item has been processed These four methods are pretty standard, in any custom iterator implementation, though Scan() will typically be replaced with an implementation-specific method to process the current result, and in some cases Err() and Close() may be combined.

Get daily content like this in your inbox!

Subscribe