Iterators and Yield

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Visual Basic 17.13 on .NET 10 (SDK 10.0.402) and was checked on 2026-10-19. Every example is a complete console program that starts with Option Strict On, was built with warnings treated as errors and was run in the .NET 10 SDK container (Linux); the output shown is real output. The examples that are meant to fail say so and show the compiler messages that the SDK printed. The project-level imports of the SDK (System, System.Collections.Generic, System.Linq, System.Threading.Tasks and others) are why those namespaces need no Imports in the examples, and the root namespace of the scratch project was ShelfDocs.

An iterator is a function (or the Get accessor of a property) that produces a sequence of values one at a time. You write it as straight-line code with loops and Yield statements, and the compiler rewrites it into a hidden class that implements IEnumerable(Of T) and IEnumerator(Of T) and remembers where the body stopped. The consumer pulls the next value with For Each, a LINQ query or a hand-written MoveNext loop, and only then does the body run up to the next Yield. This is what makes iterators the natural way to produce a sequence that is expensive, large or infinite, or that must be filtered and transformed step by step without building intermediate lists. The running example is the shelf library catalogue: books, loans and the stores that hold them.

Iterators were introduced in Visual Basic 11 (Visual Studio 2012), together with Async/Await, and the feature has not changed in syntax since. Two things are different from C#, and both are covered below. Visual Basic allows Yield inside the Try block of a Try statement that has Catch clauses, and it has no asynchronous iterators at all: an IAsyncEnumerable(Of T) can be consumed from Visual Basic, but not written with Yield.

Iterator functions

[modifiers] Iterator Function Name([parameters]) As IEnumerable(Of T) | IEnumerator(Of T) | IEnumerable | IEnumerator
    ' statements, including one or more Yield expression
End Function
  • The Iterator modifier turns a Function into an iterator. There is no Iterator Sub: an iterator always returns a sequence.

  • The return type must be IEnumerable(Of T), IEnumerator(Of T) or one of the non-generic forms IEnumerable and IEnumerator. List(Of T), arrays, Integer and IAsyncEnumerable(Of T) are rejected (BC36938).

  • Yield expression hands one element to the consumer and suspends the body. The expression must convert implicitly to the element type (T, or Object for the non-generic forms), under the usual Option Strict On rules.

  • The iterator ends when execution reaches End Function, an Exit Function or a Return statement without a value. Return value is rejected with BC36942 ("To return a value from an Iterator function, use 'Yield' rather than 'Return'"), because the function produces its values through Yield.

  • Yield is not a reserved word. It has its special meaning only inside an iterator, and in an ordinary procedure Yield 3 is a syntax error (BC30800, because the compiler reads Yield as a procedure name).

  • An iterator cannot have ByRef parameters (BC36927), because the state machine keeps the parameters in fields. It cannot be an event accessor, an instance constructor or a static constructor.

  • Iterator can be combined with Public, Private, Shared, Overridable, Overrides and Implements, like any other function. It cannot be combined with Async (BC36936).

The first example shows the central property of an iterator: calling it runs none of its body. The Console.WriteLine calls expose when each part of the body runs.

Option Strict On

Public Class Book
    Public ReadOnly Property Title As String
    Public ReadOnly Property Year As Integer

    Public Sub New(title As String, year As Integer)
        Me.Title = title
        Me.Year = year
    End Sub
End Class

Public Class Catalog
    Private ReadOnly _books As New List(Of Book)

    Public Sub Add(book As Book)
        _books.Add(book)
    End Sub

    ' An iterator function: the compiler turns it into a state machine.
    Public Iterator Function BooksPublishedSince(year As Integer) As IEnumerable(Of Book)
        Console.WriteLine("  [iterator starts]")
        For Each book In _books
            Console.WriteLine("  [looking at " & book.Title & "]")
            If book.Year >= year Then
                Yield book
            End If
        Next
        Console.WriteLine("  [iterator ends]")
    End Function
End Class

Module Program
    Sub Main()
        Dim catalog As New Catalog()
        catalog.Add(New Book("Compilers", 1986))
        catalog.Add(New Book("Refactoring", 1999))
        catalog.Add(New Book("Domain-Driven Design", 2003))

        Console.WriteLine("Calling the iterator function...")
        Dim recent = catalog.BooksPublishedSince(1999)
        Console.WriteLine("...it has returned; nothing ran yet.")

        Console.WriteLine("Starting the For Each:")
        For Each book In recent
            Console.WriteLine("got " & book.Title)
        Next
    End Sub
End Module
Calling the iterator function...
...it has returned; nothing ran yet.
Starting the For Each:
  [iterator starts]
  [looking at Compilers]
  [looking at Refactoring]
got Refactoring
  [looking at Domain-Driven Design]
got Domain-Driven Design
  [iterator ends]

Source: Yield Statement

Read the output from the top. The call catalog.BooksPublishedSince(1999) returned a recent variable and printed nothing else, because it only constructs the state machine and stores the arguments. The first MoveNext, called by For Each, ran the body up to the first Yield. Each later MoveNext resumed after the Yield that had last run, with every local variable, the position in the inner For Each and the argument year intact, and ran to the next Yield. The line [iterator ends] appeared only when the loop asked for one more element after the last book.

Deferred execution

Because the body runs on demand, an iterator gives you deferred execution (lazy evaluation), the same model as the LINQ operators in LINQ and Query Expressions. The consequences are worth stating exactly, because they cause most of the surprises in practice.

  • Nothing runs when the iterator function is called, including argument checks and any side effect.

  • Every new enumeration runs the body again from the start. An expensive iterator that is consumed twice does the work twice.

  • The body reads the data it uses (a list, a field, a captured variable) when the loop reaches it, not when the iterator function was called.

  • If the underlying collection changes while an enumeration is under way, the collection decides what happens; List(Of T) throws InvalidOperationException.

Option Strict On

Module Program
    Private _calls As Integer

    Private Iterator Function Numbered(count As Integer) As IEnumerable(Of Integer)
        _calls += 1
        Dim run = _calls
        For i = 1 To count
            Console.WriteLine("    run " & run & " produces " & i)
            Yield i
        Next
    End Function

    Sub Main()
        ' 1. The sequence is re-created on every enumeration.
        Dim numbers = Numbered(2)
        Console.WriteLine("first Count:  " & numbers.Count())
        Console.WriteLine("second Count: " & numbers.Count())

        ' 2. Materialise once with ToList when the work is expensive or has side effects.
        Dim snapshot = Numbered(2).ToList()
        Console.WriteLine("snapshot:     " & snapshot.Count & " and " & snapshot.Count)

        ' 3. The iterator sees the source as it is when the loop reaches it,
        '    not as it was when the iterator function was called.
        Dim source As New List(Of String) From {"a", "b"}
        Dim upper = Upcase(source)
        source.Add("c")
        Console.WriteLine("late binding: " & String.Join(",", upper))

        ' 4. Changing the list that is being iterated is detected by the list.
        Try
            For Each s In Upcase(source)
                If s = "A" Then source.Add("d")
            Next
        Catch ex As InvalidOperationException
            Console.WriteLine("InvalidOperationException: " & ex.Message)
        End Try
    End Sub

    Private Iterator Function Upcase(items As List(Of String)) As IEnumerable(Of String)
        For Each item In items
            Yield item.ToUpperInvariant()
        Next
    End Function
End Module
    run 1 produces 1
    run 1 produces 2
first Count:  2
    run 2 produces 1
    run 2 produces 2
second Count: 2
    run 3 produces 1
    run 3 produces 2
snapshot:     2 and 2
late binding: A,B,C
InvalidOperationException: Collection was modified; enumeration operation may not execute.

numbers.Count() ran the body twice, once for each call, and the counter _calls proves it: the second enumeration is "run 2". Numbered(2).ToList() ran it a third time and stored the two numbers, so the two following snapshot.Count reads cost nothing. The upper variable was created before "c" was added to source and still saw "c". The fourth case is the List(Of T) safeguard: adding an item to the list that an iterator is walking invalidates the iterator’s inner For Each, and the exception arrives from the consumer’s MoveNext call.

Materialise with ToList, ToArray or ToDictionary when you need a snapshot, when the source is a one-shot stream (a StreamReader, a database reader, a network response) that cannot be read twice, or when the loop body modifies the source. Code-analysis tools such as ReSharper warn about "possible multiple enumeration" of an IEnumerable(Of T) for this reason.

Iterators compose into lazy pipelines

An iterator that consumes another sequence pulls its elements on demand, so a chain of iterators moves one element at a time through every stage. This is how Where, Select and Take in System.Linq are built, and why Take(10) on an infinite source terminates.

Option Strict On

Module Program
    Private Iterator Function Source() As IEnumerable(Of Integer)
        For i = 1 To 4
            Console.WriteLine("source  yields " & i)
            Yield i
        Next
    End Function

    Private Iterator Function Filter(items As IEnumerable(Of Integer)) As IEnumerable(Of Integer)
        For Each item In items
            If item Mod 2 = 0 Then
                Console.WriteLine("filter  passes " & item)
                Yield item
            End If
        Next
    End Function

    Private Iterator Function Square(items As IEnumerable(Of Integer)) As IEnumerable(Of Integer)
        For Each item In items
            Console.WriteLine("square  yields " & item * item)
            Yield item * item
        Next
    End Function

    Sub Main()
        ' One element travels through the whole pipeline before the next one starts.
        For Each value In Square(Filter(Source()))
            Console.WriteLine("consumer got " & value)
        Next
    End Sub
End Module
source  yields 1
source  yields 2
filter  passes 2
square  yields 4
consumer got 4
source  yields 3
source  yields 4
filter  passes 4
square  yields 16
consumer got 16

No stage holds more than one element. A filter that rejects 1 and 3 never lets them reach Square, and the consumer receives 4 before the source has produced 3. Memory use stays flat however long the source is, which is the reason to prefer an iterator to a function that builds and returns a List(Of T) when the consumer may stop early or the result may be large.

Validating arguments

An argument check inside an iterator is itself deferred. LazyTake(Nothing, 1) does not throw at the call; it throws when the first element is requested, possibly far from the code that passed the wrong argument. When the check should fail at the call, split the function in two: an ordinary function that validates and returns an iterator. Visual Basic has no local functions, so the iterator becomes a second, private function.

Option Strict On

Module Program
    ' Lazy validation: the Throw runs on the first MoveNext, not at the call.
    Private Iterator Function LazyTake(source As IEnumerable(Of String), count As Integer) As IEnumerable(Of String)
        If source Is Nothing Then Throw New ArgumentNullException(NameOf(source))
        If count < 0 Then Throw New ArgumentOutOfRangeException(NameOf(count))
        Dim taken = 0
        For Each item In source
            If taken = count Then Exit For
            taken += 1
            Yield item
        Next
    End Function

    ' Eager validation: an ordinary function checks, then delegates to a private iterator.
    Private Function EagerTake(source As IEnumerable(Of String), count As Integer) As IEnumerable(Of String)
        If source Is Nothing Then Throw New ArgumentNullException(NameOf(source))
        If count < 0 Then Throw New ArgumentOutOfRangeException(NameOf(count))
        Return TakeCore(source, count)
    End Function

    Private Iterator Function TakeCore(source As IEnumerable(Of String), count As Integer) As IEnumerable(Of String)
        Dim taken = 0
        For Each item In source
            If taken = count Then Exit For
            taken += 1
            Yield item
        Next
    End Function

    Sub Main()
        Try
            Dim lazy = LazyTake(Nothing, 1)
            Console.WriteLine("LazyTake returned without complaint")
            For Each s In lazy
                Console.WriteLine(s)
            Next
        Catch ex As ArgumentNullException
            Console.WriteLine("LazyTake threw on enumeration: " & ex.ParamName)
        End Try

        Try
            Dim eager = EagerTake(Nothing, 1)
            Console.WriteLine("not reached")
        Catch ex As ArgumentNullException
            Console.WriteLine("EagerTake threw at the call:   " & ex.ParamName)
        End Try

        Console.WriteLine(String.Join(",", EagerTake({"a", "b", "c"}, 2)))
    End Sub
End Module
LazyTake returned without complaint
LazyTake threw on enumeration: source
EagerTake threw at the call:   source
a,b

Source: Iterator

ex.ParamName is source in both cases; what differs is where the exception surfaces. The library’s own operators (Take, Where, Select) use the second pattern, which is why Enumerable.Where(Nothing, ...) throws immediately even though the filtering itself is deferred.

Iterator properties and enumerable classes

The Get accessor of a property can be an iterator, which is the Visual Basic way to expose a computed sequence as a property. The property must be ReadOnly and its type must be one of the four sequence types. The Iterator modifier goes in the property declaration, next to ReadOnly; the Yield statements go in the Get block. A property with parameters works as well.

A class becomes usable in For Each when it has a GetEnumerator method that returns an enumerator, and the usual way to say so is to implement IEnumerable(Of T). Making GetEnumerator an iterator avoids writing an enumerator class by hand (with its MoveNext, Current, Reset and Dispose members). The non-generic IEnumerable.GetEnumerator has to be implemented too; it forwards to the generic one.

Option Strict On

Public Class Loan
    Public ReadOnly Property Borrower As String
    Public ReadOnly Property DueOn As Date

    Public Sub New(borrower As String, dueOn As Date)
        Me.Borrower = borrower
        Me.DueOn = dueOn
    End Sub
End Class

' Making a class usable in For Each: implement IEnumerable(Of T) with an iterator GetEnumerator.
Public Class LoanBook
    Implements IEnumerable(Of Loan)

    Private ReadOnly _loans As New List(Of Loan)

    Public Sub Add(loan As Loan)
        _loans.Add(loan)
    End Sub

    Public Iterator Function GetEnumerator() As IEnumerator(Of Loan) Implements IEnumerable(Of Loan).GetEnumerator
        For Each loan In _loans
            Yield loan
        Next
    End Function

    Private Function GetNonGenericEnumerator() As Collections.IEnumerator Implements Collections.IEnumerable.GetEnumerator
        Return GetEnumerator()
    End Function

    ' An iterator property: ReadOnly, no Set, the body is the Get accessor.
    Public ReadOnly Iterator Property Overdue(today As Date) As IEnumerable(Of Loan)
        Get
            For Each loan In _loans
                If loan.DueOn < today Then Yield loan
            Next
        End Get
    End Property

    ' A parameterless iterator property.
    Public ReadOnly Iterator Property Borrowers As IEnumerable(Of String)
        Get
            For Each loan In _loans
                Yield loan.Borrower
            Next
        End Get
    End Property
End Class

Module Program
    Sub Main()
        Dim loans As New LoanBook()
        loans.Add(New Loan("Ana", #2026-09-01#))
        loans.Add(New Loan("Luis", #2026-11-01#))
        loans.Add(New Loan("Marta", #2026-10-01#))

        For Each loan In loans
            Console.WriteLine(loan.Borrower & " due " & loan.DueOn.ToString("yyyy-MM-dd"))
        Next
        Console.WriteLine("Overdue on 2026-10-19: " & String.Join(", ", loans.Overdue(#2026-10-19#).Select(Function(l) l.Borrower)))
        Console.WriteLine("Borrowers: " & String.Join(", ", loans.Borrowers))
        Console.WriteLine("LINQ works too: " & loans.Count(Function(l) l.DueOn.Month = 10))
    End Sub
End Module
Ana due 2026-09-01
Luis due 2026-11-01
Marta due 2026-10-01
Overdue on 2026-10-19: Ana, Marta
Borrowers: Ana, Luis, Marta
LINQ works too: 1

Source: Iterator

  • LoanBook is enumerable with For Each and, because it implements IEnumerable(Of Loan), with every LINQ operator (Count, Where, OrderBy, …​).

  • Overdue(today) is an iterator property with a parameter, and Borrowers one without. Both are ReadOnly Iterator Property; the order of the two modifiers is free.

  • The GetEnumerator iterator Implements IEnumerable(Of Loan).GetEnumerator. The non-generic method is Private so that it does not show up in IntelliSense.

  • An iterator cannot be used in an event accessor, instance constructor or static constructor, so a class that wants to produce a sequence on construction has to do it from a method or property.

Interfaces, overriding and structures

An iterator function can implement an interface member and can be Overridable; an override can call the base iterator through MyBase, and its result is just another sequence that the override extends or filters. In a Structure, the iterator works on a copy of the value: the state machine copies Me when the iterator function is called.

Option Strict On

Public Class Book
    Public ReadOnly Property Title As String
    Public ReadOnly Property Year As Integer

    Public Sub New(title As String, year As Integer)
        Me.Title = title
        Me.Year = year
    End Sub
End Class

Public Interface ICatalogStore
    Function FindByYear(year As Integer) As IEnumerable(Of Book)
End Interface

Public Class InMemoryStore
    Implements ICatalogStore

    Private ReadOnly _books As New List(Of Book)

    Public Sub New(ParamArray books As Book())
        _books.AddRange(books)
    End Sub

    ' An iterator function can implement an interface member.
    Public Overridable Iterator Function FindByYear(year As Integer) As IEnumerable(Of Book) Implements ICatalogStore.FindByYear
        For Each book In _books
            If book.Year = year Then Yield book
        Next
    End Function
End Class

Public Class ReprintStore
    Inherits InMemoryStore

    Public Sub New(ParamArray books As Book())
        MyBase.New(books)
    End Sub

    ' An overriding iterator can call the base iterator through MyBase and add to its results.
    Public Overrides Iterator Function FindByYear(year As Integer) As IEnumerable(Of Book)
        For Each book In MyBase.FindByYear(year)
            Yield book
        Next
        Yield New Book("(reprint of " & year & ")", year)
    End Function
End Class

Module Program
    Sub Main()
        Dim store As ICatalogStore = New InMemoryStore(
            New Book("Refactoring", 1999), New Book("Compilers", 1986), New Book("Patterns", 1999))
        For Each book In store.FindByYear(1999)
            Console.WriteLine(book.Title)
        Next
        Dim reprints As ICatalogStore = New ReprintStore(New Book("Compilers", 1986))
        Console.WriteLine(String.Join("; ", reprints.FindByYear(1986).Select(Function(b) b.Title)))
    End Sub
End Module
Refactoring
Patterns
Compilers; (reprint of 1986)

Source: Iterator

Option Strict On

Public Structure Window
    Public Start As Integer

    Public Iterator Function Values() As IEnumerable(Of Integer)
        Yield Start
        Yield Start + 1
    End Function
End Structure

Public Class WindowClass
    Public Start As Integer

    Public Iterator Function Values() As IEnumerable(Of Integer)
        Yield Start
        Yield Start + 1
    End Function
End Class

Module Program
    Sub Main()
        Dim w As New Window With {.Start = 5}
        Dim fromStructure = w.Values()
        w.Start = 100
        Console.WriteLine("structure: " & String.Join(",", fromStructure))

        Dim c As New WindowClass With {.Start = 5}
        Dim fromClass = c.Values()
        c.Start = 100
        Console.WriteLine("class:     " & String.Join(",", fromClass))
    End Sub
End Module
structure: 5,6
class:     100,101

Source: Iterator

For the class, the iterator reads Start when the first MoveNext runs, so it sees the later assignment (100,101). For the structure, the copy of Me was made when w.Values() was called, so the later assignment is not seen (5,6). If a structure’s iterator must see later changes, make the type a class, or have the consumer call the iterator after the changes (see Structures and Value Types).

Return types and what the compiler generates

The four return types differ in what they hand back, and in how the caller uses the result.

Return type Element type of Yield Use

IEnumerable(Of T)

T

The normal choice: the result works with For Each and LINQ, and can be enumerated more than once (each time re-running the body)

IEnumerator(Of T)

T

A GetEnumerator implementation (see above) or a hand-driven enumerator; not usable in For Each itself

IEnumerable

Object

Non-generic collections, Implements IEnumerable; the loop variable is Object

IEnumerator

Object

A non-generic GetEnumerator

Option Strict On

Module Program
    Private Iterator Function Generic() As IEnumerable(Of Integer)
        Yield 1
        Yield 2
    End Function

    Private Iterator Function NonGeneric() As IEnumerable
        Yield 1
        Yield "two"
        Yield 3.0
    End Function

    Private Iterator Function EnumeratorOnly() As IEnumerator(Of Integer)
        Yield 1
    End Function

    Sub Main()
        ' What the compiler returns is a hidden class implementing both interfaces.
        Dim sequence = Generic()
        Console.WriteLine("is IEnumerator(Of Integer): " & (TypeOf sequence Is IEnumerator(Of Integer)))
        Console.WriteLine("GetEnumerator returns the same object the first time: " &
                          ReferenceEquals(sequence, sequence.GetEnumerator()))
        Console.WriteLine("... and a new object the second time: " &
                          Not ReferenceEquals(sequence, sequence.GetEnumerator()))

        ' Current before the first MoveNext is the default value; after the end it keeps the last value (do not rely on either).
        Dim e = Generic().GetEnumerator()
        Console.WriteLine("before MoveNext: " & e.Current)
        While e.MoveNext()
        End While
        Console.WriteLine("after the end:   " & e.Current & " (MoveNext = " & e.MoveNext() & ")")

        ' Reset is not supported.
        Try
            e.Reset()
        Catch ex As NotSupportedException
            Console.WriteLine("Reset: NotSupportedException")
        End Try

        ' The non-generic form yields Object, so the loop variable is Object.
        For Each item In NonGeneric()
            Console.WriteLine(item.GetType().Name & ": " & item.ToString())
        Next

        ' An iterator that returns IEnumerator is not itself usable in For Each.
        Dim only = EnumeratorOnly()
        Console.WriteLine("EnumeratorOnly is IEnumerable: " & (TypeOf only Is IEnumerable))
    End Sub
End Module
is IEnumerator(Of Integer): True
GetEnumerator returns the same object the first time: True
... and a new object the second time: True
before MoveNext: 0
after the end:   2 (MoveNext = False)
Reset: NotSupportedException
Int32: 1
String: two
Double: 3
EnumeratorOnly is IEnumerable: False
  • The hidden class implements both IEnumerable(Of T) and IEnumerator(Of T). The first call to GetEnumerator on the thread that created the sequence returns the same object, and any later call returns a new, independent enumerator. Do not depend on this; it is an optimisation, not a contract.

  • Current is meaningful only between a MoveNext that returned True and the next MoveNext. Before the first MoveNext it holds the default value, and the value after the end is unspecified.

  • Reset throws NotSupportedException: ask the IEnumerable(Of T) for a new enumerator instead.

  • An iterator that returns IEnumerator(Of T) is only an enumerator. For Each needs an IEnumerable, so a function that returns IEnumerator can be used from For Each only through a class that exposes it from GetEnumerator.

The Yield expression has to convert implicitly to the element type. Widening is fine (Yield an Integer in a sequence of Long), narrowing is not under Option Strict On:

Option Strict On

Module Program
    Private Iterator Function Lossy() As IEnumerable(Of Integer)
        Dim half As Double = 2.5
        Yield half
        Yield "3"
    End Function

    Sub Main()
        For Each n In Lossy()
            Console.WriteLine(n)
        Next
    End Sub
End Module
Program.vb(6,15): error BC30512: Option Strict On disallows implicit conversions from 'Double' to 'Integer'.
Program.vb(7,15): error BC30512: Option Strict On disallows implicit conversions from 'String' to 'Integer'.

Source: Yield Statement

Compile-time rules

The compiler checks the shape of an iterator in two passes. The signature errors come first, then the errors about what the body contains; the second kind appears only after the first kind is fixed, so a file can fail twice. Both examples below were built exactly as listed and print the compiler’s messages; the line numbers refer to the listings.

Option Strict On

Module Program
    Iterator Function ByRefParameter(ByRef x As Integer) As IEnumerable(Of Integer)
        Yield x
    End Function

    Iterator Function WrongReturnType() As List(Of Integer)
        Yield 1
    End Function

    Iterator Function ScalarReturn() As Integer
        Yield 1
    End Function

    Async Iterator Function Both() As IAsyncEnumerable(Of Integer)
        Yield 1
    End Function

    Sub Main()
    End Sub
End Module
Program.vb(4,38): error BC36927: Iterator methods cannot have ByRef parameters.
Program.vb(8,44): error BC36938: Iterator functions must return either IEnumerable(Of T), or IEnumerator(Of T), or the non-generic forms IEnumerable or IEnumerator.
Program.vb(12,41): error BC36938: Iterator functions must return either IEnumerable(Of T), or IEnumerator(Of T), or the non-generic forms IEnumerable or IEnumerator.
Program.vb(16,5): error BC36936: 'Async' and 'Iterator' modifiers cannot be used together.

Source: Iterator

With those four fixed, the body checks run:

Option Strict On

Module Program
    Iterator Function InCatch() As IEnumerable(Of Integer)
        Try
            Yield 1
        Catch ex As Exception
            Yield 2
        End Try
    End Function

    Iterator Function InFinally() As IEnumerable(Of Integer)
        Try
            Yield 1
        Finally
            Yield 2
        End Try
    End Function

    Sub Main()
    End Sub
End Module
Program.vb(8,13): error BC36939: 'Yield' cannot be used inside a 'Catch' statement or a 'Finally' statement.
Program.vb(16,13): error BC36939: 'Yield' cannot be used inside a 'Catch' statement or a 'Finally' statement.

Source: Yield Statement

Error Meaning Fix

BC36942

Return value in an iterator

Use Yield value, then Return or Exit Function without a value

BC36927

An iterator method cannot have ByRef parameters

Pass ByVal, or return the value in the sequence

BC36938

The return type is not one of the four sequence types

Return IEnumerable(Of T), or build the list and return it from a normal function

BC36936

Async and Iterator cannot be combined

Visual Basic has no asynchronous iterators; see the last section of this page

BC36939

Yield inside Catch or Finally

Move the Yield into the Try block, or collect the value in a variable and yield it after the Try statement

BC32023

For Each over an IAsyncEnumerable(Of T)

Use the explicit protocol in the last section

BC30512

The Yield expression does not convert implicitly

Convert explicitly, as everywhere under Option Strict On

BC36674, BC30205

The lambda was written on one line

An iterator lambda must be multi-line with End Function; see below

Try, Catch, Finally and Using inside iterators

A Yield can appear in the Try block of a Try statement, and that statement may have Catch clauses and a Finally block. A Yield inside a Catch or a Finally block is BC36939. This is more permissive than C#, where yield return is rejected in any try block that has a catch, and as strict as C# for catch and finally blocks themselves.

What each construct does when the iterator is suspended or abandoned:

  • A Finally block and a Using block run when the iterator finishes normally, when the consumer leaves the loop early (Exit For, Return, an exception in the loop body, a LINQ operator such as First or Take that stops reading), and when Dispose is called on the enumerator. For Each and the LINQ operators call Dispose for you.

  • They do not run if the consumer drives the enumerator by hand and never disposes it. A manual GetEnumerator/MoveNext loop needs its own Using (or Try/Finally).

  • A Catch clause in the iterator catches only exceptions raised by the iterator’s own code, such as the Integer.Parse call below. An exception thrown by the consumer’s loop body does not travel into the iterator.

Option Strict On

Public Class Reader
    Implements IDisposable

    Public Sub New()
        Console.WriteLine("  reader opened")
    End Sub

    Public Sub Dispose() Implements IDisposable.Dispose
        Console.WriteLine("  reader closed")
    End Sub

    Public Function ReadLine(index As Integer) As String
        Return "line " & index
    End Function
End Class

Module Program
    ' Finally and Using run when the consumer stops early, because For Each disposes the enumerator.
    Private Iterator Function Lines(count As Integer) As IEnumerable(Of String)
        Using reader As New Reader()
            Try
                For i = 1 To count
                    Yield reader.ReadLine(i)
                Next
            Finally
                Console.WriteLine("  finally block")
            End Try
        End Using
    End Function

    ' Yield is allowed in the Try block of a Try...Catch, and the Catch sees only exceptions from the iterator's own code.
    Private Iterator Function Parsed(texts As IEnumerable(Of String)) As IEnumerable(Of Integer)
        For Each entry In texts
            Dim value As Integer
            Try
                value = Integer.Parse(entry)
                Yield value
            Catch ex As FormatException
                Console.WriteLine("  skipped '" & entry & "'")
            End Try
        Next
    End Function

    Sub Main()
        Console.WriteLine("Full enumeration:")
        For Each line In Lines(2)
            Console.WriteLine(line)
        Next

        Console.WriteLine("Exit For after the first item:")
        For Each line In Lines(5)
            Console.WriteLine(line)
            Exit For
        Next

        Console.WriteLine("First() stops early too:")
        Console.WriteLine(Lines(5).First())

        Console.WriteLine("Manual enumerator, never disposed -> nothing is cleaned up:")
        Dim e = Lines(5).GetEnumerator()
        e.MoveNext()
        Console.WriteLine(e.Current)
        Console.WriteLine("  (no Dispose call, so no 'finally block' line)")

        Console.WriteLine("The loop body throws: the iterator's Finally and Using still run:")
        Try
            For Each line In Lines(3)
                Throw New InvalidOperationException("consumer failed on " & line)
            Next
        Catch ex As InvalidOperationException
            Console.WriteLine("  caught: " & ex.Message)
        End Try

        Console.WriteLine("Catch around Yield:")
        Console.WriteLine(String.Join(",", Parsed({"1", "x", "3"})))
    End Sub
End Module
Full enumeration:
  reader opened
line 1
line 2
  finally block
  reader closed
Exit For after the first item:
  reader opened
line 1
  finally block
  reader closed
First() stops early too:
  reader opened
  finally block
  reader closed
line 1
Manual enumerator, never disposed -> nothing is cleaned up:
  reader opened
line 1
  (no Dispose call, so no 'finally block' line)
The loop body throws: the iterator's Finally and Using still run:
  reader opened
  finally block
  reader closed
  caught: consumer failed on line 1
Catch around Yield:
  skipped 'x'
1,3

Source: Yield Statement

Compare the output of Exit For, First() and the manual enumerator. In the first two the cleanup (finally block, reader closed) runs before the program continues. In the First() case it even prints before the value is written, because First disposes the enumerator before it returns the element. The manual enumerator that was never disposed leaves the reader open, which for a FileStream or a database connection means a leak.

Dispose and MoveNext: the 17.13 change

An enumerator that was disposed must not run any more of the iterator’s code. Before Visual Basic 17.13 (the compiler that shipped with Visual Studio 2022 17.13) the state machine of an iterator that returned IEnumerator(Of T) could be resumed after Dispose: according to the compiler team’s description, execution could continue after the point where the iterator had been suspended. The compiler team listed this fix in the breaking changes for the .NET 10 SDK ("Set state of enumerator object to 'after' during disposal"). From that version on, MoveNext on a disposed enumerator returns False without running any user code.

Option Strict On

Module Program
    Private Iterator Function Steps() As IEnumerator(Of Integer)
        Console.WriteLine("  running step 1")
        Yield 1
        Console.WriteLine("  running code between the yields")
        Yield 2
        Console.WriteLine("  running step 3")
        Yield 3
    End Function

    Private Iterator Function WithCleanup() As IEnumerator(Of Integer)
        Try
            Yield 1
            Yield 2
        Finally
            Console.WriteLine("  cleanup")
        End Try
    End Function

    Sub Main()
        Dim e = Steps()
        Console.WriteLine("MoveNext: " & e.MoveNext() & ", Current = " & e.Current)
        e.Dispose()
        Console.WriteLine("MoveNext after Dispose: " & e.MoveNext())

        Dim f = WithCleanup()
        f.MoveNext()
        f.Dispose()
        f.Dispose()
        Console.WriteLine("MoveNext after second Dispose: " & f.MoveNext())

        ' A disposed iterator that never started never runs its body either.
        Dim g = Steps()
        g.Dispose()
        Console.WriteLine("MoveNext on a never-started, disposed iterator: " & g.MoveNext())
    End Sub
End Module
  running step 1
MoveNext: True, Current = 1
MoveNext after Dispose: False
  cleanup
MoveNext after second Dispose: False
MoveNext on a never-started, disposed iterator: False
  • The first MoveNext returns True; the Dispose then ends the sequence. The next MoveNext returns False and the line "running code between the yields" is never printed. That line is what a resumed state machine would have run.

  • Dispose runs the pending Finally blocks once. A second Dispose is harmless.

  • Code that only uses For Each and LINQ never sees the difference, because those never MoveNext after Dispose. The change affects hand-written loops and wrappers that keep an enumerator around after disposing it. If a program depended on the old behaviour, it relied on the state of a disposed object. The change is a compiler behaviour fix recorded in the breaking-changes document, not a new syntax feature.

Recursion, infinite sequences, generics and extension methods

Iterators can call themselves, take generic type parameters, be <Extension> methods and never end. A recursive iterator does not flatten for you: the Yield has to re-yield each element of the nested call, so a deep tree costs one nested MoveNext call per level for every element. Visual Basic has no Yield From or yield*.

Option Strict On
Imports System.Runtime.CompilerServices

Public Module SequenceExtensions
    ' A generic iterator extension method: chunks of at most "size" items.
    <Extension>
    Public Iterator Function Batch(Of T)(source As IEnumerable(Of T), size As Integer) As IEnumerable(Of List(Of T))
        Dim current As New List(Of T)(size)
        For Each item In source
            current.Add(item)
            If current.Count = size Then
                Yield current
                current = New List(Of T)(size)
            End If
        Next
        If current.Count > 0 Then Yield current
    End Function
End Module

Public Class Category
    Public ReadOnly Property Name As String
    Public ReadOnly Property Children As New List(Of Category)

    Public Sub New(name As String)
        Me.Name = name
    End Sub

    ' Recursion: an iterator re-yields what a recursive call produces, level by level.
    Public Iterator Function Descendants(Optional depth As Integer = 0) As IEnumerable(Of String)
        Yield New String(" "c, depth * 2) & Name
        For Each child In Children
            For Each line In child.Descendants(depth + 1)
                Yield line
            Next
        Next
    End Function
End Class

Module Program
    ' An infinite sequence is fine as long as the consumer stops.
    Private Iterator Function Fibonacci() As IEnumerable(Of Long)
        Dim a As Long = 0
        Dim b As Long = 1
        Do
            Yield a
            Dim next1 = a + b
            a = b
            b = next1
        Loop
    End Function

    Sub Main()
        Console.WriteLine(String.Join(" ", Fibonacci().Take(10)))
        Console.WriteLine("first Fibonacci above 1000: " & Fibonacci().First(Function(n) n > 1000))

        For Each group In Enumerable.Range(1, 7).Batch(3)
            Console.WriteLine("[" & String.Join(",", group) & "]")
        Next

        Dim root As New Category("Computing")
        Dim languages As New Category("Languages")
        languages.Children.Add(New Category("Visual Basic"))
        languages.Children.Add(New Category("C#"))
        root.Children.Add(languages)
        root.Children.Add(New Category("Databases"))
        For Each line In root.Descendants()
            Console.WriteLine(line)
        Next

        ' Driving an enumerator by hand is what For Each does for you.
        Using e = Fibonacci().GetEnumerator()
            e.MoveNext()
            e.MoveNext()
            e.MoveNext()
            Console.WriteLine("third value: " & e.Current)
        End Using
    End Sub
End Module
0 1 1 2 3 5 8 13 21 34
first Fibonacci above 1000: 1597
[1,2,3]
[4,5,6]
[7]
Computing
  Languages
    Visual Basic
    C#
  Databases
third value: 1
  • Fibonacci never ends, and Take(10) and First(...) stop it. An infinite iterator must be consumed only through something that stops (Take, TakeWhile, First, Exit For); ToList() or Count() on it never returns.

  • Batch(Of T) is an iterator extension method. Declare it in a Module, with <Extension> and the System.Runtime.CompilerServices import; see Extension Methods and Partial Methods. .NET 6 added Enumerable.Chunk, which does the same job and returns arrays; Batch returns lists.

  • In Descendants the inner loop variable is called line. Naming it name is refused with BC30039 ("Loop control variable cannot be a property or a late-bound indexed array"), because Visual Basic names are not case sensitive and name is the Name property of the class: inside a class, the property wins over a new For Each variable of the same spelling. The error was reproduced while writing this page.

Iterator lambdas

A lambda can be an iterator. Put Iterator before Function, declare the return type with As, and write the body on several lines up to End Function. There is no single-line form (BC36674 followed by BC30205), because a Yield is a statement and a single-line lambda body is an expression. The lambda converts to a delegate type such as Func(Of Integer, IEnumerable(Of Integer)) in the usual way, see Delegates and Lambda Expressions.

Option Strict On

Module Program
    Sub Main()
        ' A multi-line iterator lambda. The Iterator keyword goes before Function.
        Dim countdown = Iterator Function(start As Integer) As IEnumerable(Of Integer)
                            For i = start To 1 Step -1
                                Yield i
                            Next
                        End Function

        Console.WriteLine(String.Join(" ", countdown(3)))

        ' A closure captures a local variable; the iterator reads it when it runs, not when it is created.
        Dim limit = 2
        Dim upToLimit = Iterator Function() As IEnumerable(Of Integer)
                            For i = 1 To limit
                                Yield i
                            Next
                        End Function
        Dim sequence = upToLimit()
        limit = 4
        Console.WriteLine(String.Join(" ", sequence))

        ' Iterator lambdas are usable wherever a delegate of the right type is expected.
        Dim words As Func(Of String, IEnumerable(Of Char)) =
            Iterator Function(w As String) As IEnumerable(Of Char)
                For Each c In w
                    If Char.IsUpper(c) Then Yield c
                Next
            End Function
        Console.WriteLine(String.Join("", words("VisualBasicNet")))

        ' Inside LINQ: SelectMany with an iterator lambda.
        Dim pairs = Enumerable.Range(1, 3).SelectMany(
            Iterator Function(n As Integer) As IEnumerable(Of String)
                For m = n + 1 To 3
                    Yield n & "-" & m
                Next
            End Function)
        Console.WriteLine(String.Join(" ", pairs))
    End Sub
End Module
3 2 1
1 2 3 4
VBN
1-2 1-3 2-3

The single-line attempt that does not compile:

Option Strict On

Module Program
    Sub Main()
        Dim numbers = Iterator Function() As IEnumerable(Of Integer) Yield 5
    End Sub
End Module
Program.vb(5,23): error BC36674: Multiline lambda expression is missing 'End Function'.
Program.vb(5,70): error BC30205: End of statement expected.

Source: Iterator

The second lambda in the example captured limit as a variable, not as a value. It printed 1 2 3 4 because limit was changed to 4 before the first MoveNext, and the iterator read limit when its loop started. This is the same closure rule as for every lambda (see the delegates page), compounded by the deferred execution of iterators: a captured variable that changes before the sequence is enumerated changes the sequence.

Consuming IAsyncEnumerable(Of T) from Visual Basic

Asynchronous streams (IAsyncEnumerable(Of T), IAsyncEnumerator(Of T) and IAsyncDisposable) exist in .NET since .NET Core 3.0, and the base class library returns them from APIs such as ChannelReader(Of T).ReadAllAsync, HttpContent.ReadFromJsonAsAsyncEnumerable and the Entity Framework Core AsAsyncEnumerable. Visual Basic supports none of the language features built on them:

  • There is no Await For Each. For Each on an IAsyncEnumerable(Of T) is BC32023 ("Expression is of type 'IAsyncEnumerable(Of Integer)', which is not a collection type"), because the interface is not IEnumerable.

  • There is no Await Using. Using calls the synchronous Dispose, and IAsyncDisposable does not have one.

  • There are no asynchronous iterators. Async Iterator Function ... As IAsyncEnumerable(Of T) is BC36936, and an Async function whose return type is not Task, Task(Of T) or Sub is BC36945, including ValueTask(Of T).

  • Await is not allowed in a Catch or Finally block (BC36943), unlike C#. That is the obstacle for the obvious hand-written translation of await foreach, which would put Await enumerator.DisposeAsync() in a Finally.

Option Strict On

Module Program
    Private Async Function CleanUpAsync(resource As IAsyncDisposable) As Task
        Try
            Await Task.Delay(1)
        Finally
            Await resource.DisposeAsync()
        End Try
    End Function

    Sub Main()
    End Sub
End Module
Program.vb(8,13): error BC36943: 'Await' cannot be used inside a 'Catch' statement, a 'Finally' statement, or a 'SyncLock' statement.

Source: Await Operator

Option Strict On

Module Program
    Private Async Function NextAsync() As ValueTask(Of Boolean)
        Await Task.Delay(1)
        Return True
    End Function

    Sub Main()
    End Sub
End Module
Program.vb(4,43): error BC36945: The 'Async' modifier can only be used on Subs, or on Functions that return Task or Task(Of T).

Source: Async

What works is the explicit protocol: get the enumerator, loop on Await enumerator.MoveNextAsync(), remember any exception in a Catch, dispose after the Try statement, and rethrow with ExceptionDispatchInfo so that the original stack trace survives.

Option Strict On
Imports System.Runtime.ExceptionServices
Imports System.Threading
Imports System.Threading.Channels

Module Program
    Sub Main()
        RunAsync().GetAwaiter().GetResult()
    End Sub

    ' VB has no "Await For Each", and Await is not allowed in Catch or Finally,
    ' so the loop that C# writes as "await foreach" is written out by hand:
    ' remember the exception, dispose asynchronously after the Try, then rethrow.
    Private Async Function ConsumeAsync(source As IAsyncEnumerable(Of String)) As Task(Of Integer)
        Dim count = 0
        Dim failure As Exception = Nothing
        Dim enumerator = source.GetAsyncEnumerator(CancellationToken.None)
        Try
            While Await enumerator.MoveNextAsync()
                Console.WriteLine("received " & enumerator.Current)
                count += 1
            End While
        Catch ex As Exception
            failure = ex
        End Try
        Await enumerator.DisposeAsync()
        If failure IsNot Nothing Then ExceptionDispatchInfo.Capture(failure).Throw()
        Return count
    End Function

    Private Async Function RunAsync() As Task
        Dim channel As Channel(Of String) = Channels.Channel.CreateUnbounded(Of String)()

        Dim producer = Task.Run(
            Async Function() As Task
                For Each title In {"Compilers", "Refactoring", "Domain-Driven Design"}
                    Await channel.Writer.WriteAsync(title)
                    Await Task.Delay(10)
                Next
                channel.Writer.Complete()
            End Function)

        Dim total = Await ConsumeAsync(channel.Reader.ReadAllAsync())
        Await producer
        Console.WriteLine("total: " & total)
    End Function
End Module
received Compilers
received Refactoring
received Domain-Driven Design
total: 3

The Channel producer is a Task.Run with an Async Function lambda that writes three titles and completes the writer; ReadAllAsync() is the IAsyncEnumerable(Of String) that ConsumeAsync reads. Notice Channels.Channel.CreateUnbounded(Of String)() with its namespace written out: the local variable is called channel, and since names are not case sensitive it would hide the Channel class if the call were written without the namespace.

If you consume many streams, wrap the protocol in a generic extension method. The next example also shows the other direction (producing a stream) the only way Visual Basic can: implementing IAsyncEnumerable(Of T) and IAsyncEnumerator(Of T) by hand, with MoveNextAsync returning ValueTask(Of Boolean) built from a private Async Function that returns Task(Of Boolean). It also shows the query operators of System.Linq.AsyncEnumerable, which are part of .NET 10 and work on any IAsyncEnumerable(Of T). Two traps in the implementation: the interface method has an optional CancellationToken parameter that the Implements method must repeat with Optional (without it BC30401 and BC30149 are reported), and the Async modifier cannot be put on the ValueTask(Of Boolean) method.

Option Strict On
Imports System.Runtime.CompilerServices
Imports System.Runtime.ExceptionServices
Imports System.Threading

Public Module AsyncSequenceExtensions
    ' A reusable stand-in for "Await For Each": run an action for every element.
    <Extension>
    Public Async Function ForEachAsync(Of T)(source As IAsyncEnumerable(Of T), action As Action(Of T)) As Task
        Dim failure As Exception = Nothing
        Dim enumerator = source.GetAsyncEnumerator(CancellationToken.None)
        Try
            While Await enumerator.MoveNextAsync()
                action(enumerator.Current)
            End While
        Catch ex As Exception
            failure = ex
        End Try
        Await enumerator.DisposeAsync()
        If failure IsNot Nothing Then ExceptionDispatchInfo.Capture(failure).Throw()
    End Function
End Module

' VB cannot write an asynchronous iterator, but it can implement the interfaces by hand.
Public Class CountdownStream
    Implements IAsyncEnumerable(Of Integer)

    Private ReadOnly _from As Integer

    Public Sub New(from As Integer)
        _from = from
    End Sub

    Public Function GetAsyncEnumerator(Optional cancellationToken As CancellationToken = Nothing) As IAsyncEnumerator(Of Integer) _
        Implements IAsyncEnumerable(Of Integer).GetAsyncEnumerator
        Return New Enumerator(_from)
    End Function

    Private NotInheritable Class Enumerator
        Implements IAsyncEnumerator(Of Integer)

        Private _next As Integer

        Public Sub New(from As Integer)
            _next = from
            Current = 0
        End Sub

        Public Property Current As Integer Implements IAsyncEnumerator(Of Integer).Current

        ' Async cannot return ValueTask(Of T) in Visual Basic: wrap a Task(Of T) instead.
        Public Function MoveNextAsync() As ValueTask(Of Boolean) Implements IAsyncEnumerator(Of Integer).MoveNextAsync
            Return New ValueTask(Of Boolean)(AdvanceAsync())
        End Function

        Private Async Function AdvanceAsync() As Task(Of Boolean)
            Await Task.Delay(1)
            If _next < 0 Then Throw New InvalidOperationException("below zero")
            If _next = 0 Then Return False
            Current = _next
            _next -= 1
            Return True
        End Function

        Public Function DisposeAsync() As ValueTask Implements IAsyncDisposable.DisposeAsync
            Console.WriteLine("  async enumerator disposed")
            Return ValueTask.CompletedTask
        End Function
    End Class
End Class

Module Program
    Sub Main()
        RunAsync().GetAwaiter().GetResult()
    End Sub

    Private Async Function RunAsync() As Task
        Await New CountdownStream(3).ForEachAsync(Sub(n) Console.WriteLine("tick " & n))

        ' System.Linq.AsyncEnumerable (part of .NET 10) supplies the query operators.
        Dim evens = Await New CountdownStream(6).Where(Function(n) n Mod 2 = 0).ToListAsync()
        Console.WriteLine("evens: " & String.Join(",", evens))

        ' The exception is rethrown after the asynchronous dispose.
        Try
            Await New CountdownStream(-1).ForEachAsync(Sub(n) Console.WriteLine("never " & n))
        Catch ex As InvalidOperationException
            Console.WriteLine("caught: " & ex.Message)
        End Try
    End Function
End Module
tick 3
tick 2
tick 1
  async enumerator disposed
  async enumerator disposed
evens: 6,4,2
  async enumerator disposed
caught: below zero

Hand-written asynchronous enumerators are boilerplate that C# generates for you. When a library needs to produce asynchronous streams, write that piece in C# (an async IAsyncEnumerable<T> method with yield return) in a separate project and consume it from Visual Basic as shown; see Interop with C# and Other .NET Languages. For work that does not need streaming, Task(Of IEnumerable(Of T)), a Channel(Of T) or an event are simpler. See Async and Await for the rest of the asynchronous model.

Performance and threading notes

  • An iterator allocates its state machine object on the first call and again for each enumerator after the first, which is cheap but not free. An iterator over an array in a hot loop is slower than a For loop over the array; use the iterator when the laziness, early exit or composition is worth it.

  • The hidden class holds the parameters and locals as fields, so a local that references a large object keeps it alive until the enumeration ends or the enumerator is collected.

  • Iterator enumerators are not thread safe. Two threads that call MoveNext on the same enumerator race on its state, and the iterator function itself can be enumerated from several threads only if each thread calls GetEnumerator on its own and the body is safe to run concurrently.

  • SyncLock can contain a Yield, and the lock is then held for as long as the consumer pauses the sequence between two elements, which is rarely what you want. Take the lock around a copy of the data and yield from the copy.

  • Trimming and native AOT: an iterator compiles to an ordinary class and does not use reflection at run time, so it needs no trimming or AOT annotations.

Visual Basic and C# side by side

Topic Visual Basic C#

Declaring an iterator

Iterator Function ... As IEnumerable(Of T)

any method returning IEnumerable<T> or IEnumerator<T> that contains yield

Produce a value

Yield expression

yield return expression;

End the sequence

Exit Function or Return

yield break;

Iterator property

ReadOnly Iterator Property ... Get

a get accessor with yield return

Iterator lambda

Iterator Function() As IEnumerable(Of T) on several lines

not allowed; use a local function

Local helper for eager validation

a second private function

a local function inside the method

Yield in Try with Catch

allowed

compile error (CS1626)

Yield in Catch or Finally

BC36939

compile error

Re-yield a nested sequence

loop over it with For Each

loop over it with foreach

Asynchronous iterators

not available

async IAsyncEnumerable<T> method with yield return

Consume an asynchronous stream

GetAsyncEnumerator and Await MoveNextAsync by hand

await foreach

Await in Catch or Finally

BC36943

allowed

Iterator in Structure

works on a copy of Me

works on a copy of this

Older material you may meet

  • Before Visual Studio 2012 (Visual Basic 10 and earlier) there were no iterators. Tutorials from that period make a class enumerable with a hand-written IEnumerator class holding an index, a MoveNext method that increments it, a Current property and an unusable Reset. That code still compiles, and Iterator replaces all of it with one function.

  • Visual Basic 6 made a class usable in For Each with a hidden NewEnum member ([_NewEnum], flagged with the procedure ID -4) that returned an IUnknown for an IEnumVARIANT. In .NET the same job is GetEnumerator. See Migrating Legacy Visual Basic.

  • Books and articles written for .NET Framework or for Visual Basic before 2012 present Yield as a new feature or omit it. It is stable, is not tied to a LangVersion, and has gained no syntax since; the only compiler change since is the 17.13 fix for Dispose described above. The Learn page for the Iterator modifier was last updated in 2021; its examples use ByVal (which is the default and is not written in new code) and Console.ReadKey (which is not needed in a console project).

Pitfalls

Pitfall What to do

Argument checks in an iterator run late

Validate in an ordinary function and return a private iterator

The iterator re-runs for every enumeration

ToList() once, or return a List(Of T) or IReadOnlyList(Of T) when the data is small

Possible multiple enumeration

Do not call Count(), Any() and For Each on the same sequence

A Finally block did not run

Dispose manually driven enumerators with Using

The loop variable is named like a property of the class

Use a different name (BC30039)

Yield in Catch or Finally

BC36939; hold the value in a variable and yield after the Try

An infinite iterator hangs a ToList

Always consume it through Take, TakeWhile, First or Exit For

Captured variable changes before the loop

Copy it to a local inside the function that creates the iterator

Await For Each and Await Using do not exist

Use the explicit protocol shown above, or write the producer in C#

Mixing IEnumerable(Of T) with a mutating source

Iterate over a copy (ToList()), or lock the source for the whole enumeration