In an iter.Seq2[string, error] directory walker, how do you report a read failure to the ranging caller?
answer
- the factory returns before anything is read
- two values: one is the error
- value half is the zero value
- decide: terminal or per-entry
- the error yield returns a bool too
basics
~20 sCall yield with a zero value for the first half and the non-nil error for the second, then either return or continue past the failed entry. The caller checks the error before using the value.
solid answer
~50 sThe function that builds the iterator returns before anything is read, so it has no error to return — failures happen during the loop and must ride in the sequence. With `iter.Seq2[string, error]` you call `yield("", err)` : the value half is the zero value, the error half is non-nil, and the caller's loop body checks the error first. The real design decision is what happens next. If the failure is fatal to the whole walk, yield it and `return`. If it is local — one unreadable subdirectory in a tree you are still walking — yield it and carry on with the siblings, which lets the caller decide whether to break. Either way you still honour yield's bool: an error yield can be refused too, and calling yield again after that is a panic. Whichever rule you pick, document it, because callers cannot infer it.
code
go · 14 linesfunc Entries(root string) iter.Seq2[string, error] {
return func(yield func(string, error) bool) {
entries, err := os.ReadDir(root)
if err != nil {
yield("", err) // zero value, real error, then stop
return
}
for _, e := range entries {
if !yield(filepath.Join(root, e.Name()), nil) {
return
}
}
}
}go deeper
Know that iter.Seq2 yields two values and that the second is commonly an error, which the loop body must check before using the first.
Explain why the error cannot come back from the function that builds the iterator, and say what you pass as the value half when the error is non-nil.
Decide and document whether a given failure ends the sequence or is per-entry and recoverable, and make sure yield's bool is still honoured on the error yield itself.
Own the convention across the package: errors in Seq2 put the check in every caller's loop body, and once teams import it that loop shape cannot be quietly changed.
## Why the error cannot be returned The natural instinct is `func Walk(root string) (iter.Seq[string], error)`. It does not work for anything real: the factory returns immediately and reads nothing, so at the moment it would return an error there is no error yet. Failures happen while the loop runs — a directory becomes unreadable, a file disappears, a decode fails on line 40,000. The error has to travel out through the loop, and `iter.Seq2[V, error]` is the standard-library-shaped way to do that. (There is a second convention — a sequence of plain values plus a terminal `Err()` method on the iterator object, the way a scanner works — but that changes the exported type and is an API decision rather than a mechanic.) ## The mechanic A `Seq2` yields a pair. When something fails: ``` if err != nil { yield("", err) // zero value, real error return } ``` Three details matter. **Yield the zero value alongside the error.** Callers write `if err != nil { ... }` and expect the value half to be meaningless in that case. Yielding a half-built value invites callers to use garbage when they forget the check, and makes the contract impossible to state. **Check the bool on the error yield too.** `yield("", err)` returns a bool exactly like any other call. A caller who breaks on the first error makes it false, and calling yield again after that panics at run time. If you yield an error and then keep walking, guard it: `if !yield("", err) { return }`. **Decide whether the sequence continues.** This is the part interviewers push on, because it is a genuine choice: - *Terminal.* Opening the root failed; there is nothing left to walk. Yield the error, return, done. - *Per-entry, recoverable.* One subdirectory of a large tree is unreadable. Yielding the error and continuing with its siblings is more useful: the caller gets both the failures and the results, and can break out itself if it does not want partial data. A walker that stops the entire walk on one permission-denied directory is often useless in production; a walker that silently skips it is worse, because the caller cannot tell a complete answer from a partial one. Yielding the error and continuing is usually the right default for a tree walk, and it is exactly what the callback-based `filepath.WalkDir` does by passing the error to its function and letting the function decide. ## What the caller writes ``` for path, err := range Walk(root) { if err != nil { log.Print(err) continue // or break, if partial results are useless } use(path) } ``` Note what the loop shape buys you: `continue` versus `break` is the caller's decision, expressed in ordinary Go, in the place where they know whether partial data is acceptable. That is the main argument for putting errors in the sequence rather than swallowing them. ## The failure modes **Silently ending the sequence on error.** The loop ends, the caller sees a short list, and nothing distinguishes "the tree has three files" from "the tree has three thousand files and we could not read the fourth directory". This is the worst outcome and the most common one, because it is what happens if you write `if err != nil { return }` with no yield. **Panicking inside the iterator.** A panic crosses the yield call and unwinds through the caller's loop. A library that panics on an I/O error is not usable; reserve panics for programmer errors. **Logging instead of reporting.** The library writes to its own logger, the caller gets a partial result with no signal. The caller owns the policy; hand them the error. **Yielding the same error forever.** An iterator that hits an error and loops on it produces an infinite sequence of identical failures. If you continue after an error, always continue past the *item* that failed. ## Errors and cleanup together The last error is often discovered after the loop over the source has ended — a stream reader that finishes normally but has a deferred read error to report. The pattern is to yield it after the production loop: ``` for sc.Scan() { if !yield(sc.Text(), nil) { return } } if err := sc.Err(); err != nil { yield("", err) } ``` The final `yield` needs no bool check because nothing follows it. ## The rule to state out loud Document, in the doc comment of every exported `Seq2` that carries errors, two sentences: which errors end the sequence, and whether the value half is ever meaningful when the error is non-nil. Callers write their loop body once, against that promise, and never revisit it.
- Should the walker keep going after it yields an error, or stop?It depends on whether the failure is fatal to the whole sequence. If the root cannot be opened there is nothing to continue with, so yield and return. If one subdirectory is unreadable, yielding the error and continuing with its siblings is usually more useful, because the caller can `continue` or `break` in its own loop. Whichever you choose, document it.
- What must the caller's loop body do first?Check the error half before touching the value half. When the error is non-nil the value is the zero value and means nothing. The caller then chooses `continue` to tolerate partial results or `break` to abandon the walk — and a `break` makes your next yield return false, which you must still honour.
- Why not just return (iter.Seq2[...], error) from the factory?Because the factory does no work: it builds a closure and returns before the first directory is read, so its error is always nil and gives false confidence. The only failures worth reporting happen inside the loop, which is why they have to travel through the sequence itself.
saying these in an interview costs you the question
- Returns silently on error so the caller sees a short result that looks complete
- Panics inside the iterator instead of yielding the error
- Yields a half-built value alongside a non-nil error
- Logs the failure inside the library and reports nothing to the caller
- Ignores the bool returned by the error yield and keeps producing
- Loops on the failing item and yields the same error forever