Yield return with try catch, how can I solve it?

47.8k views Asked by At

I've a piece of code:

using (StreamReader stream = new StreamReader(file.OpenRead(), Encoding))
{
    char[] buffer = new char[chunksize];
    while (stream.Peek() >= 0)
    {
       int readCount = stream.Read(buffer, 0, chunksize);

       yield return new string(buffer, 0, readCount);
    }
 }

Now I have to surround this with an try-catch block

try
{
   using (StreamReader stream = new StreamReader(file.OpenRead(), Encoding))
   {
       char[] buffer = new char[chunksize];
       while (stream.Peek() >= 0)
       {
          int readCount = stream.Read(buffer, 0, chunksize);

          yield return new string(buffer, 0, readCount);
       }
    } 
}
catch (Exception ex)
{
    throw ExceptionMapper.Map(ex, file.FullName)
}

I can't see any way to do what I want.

The method has the signature:

public IEnumerable<string> ReadPieces(int pieces)

I need a try/catch with a call to the ExceptionMapper in the catch case. The method is used deferred by all callers.

The exceptions I have to catch are coming from these calls:

File.OpenRead()
stream.Read()
11

There are 11 answers

1
Jason Kleban On BEST ANSWER

Because you want to keep the Stream open for the duration of the enumeration AND deal with exceptions AND properly close the file handle either way, I don't think you can use a regular enumeration shortcut (the iterator block, yield-return/yield-break).

Instead, just do what the compiler would have done for you and add some:

By implementing IEnumerator yourself, you can also add IDisposable

public class LazyStream : IEnumerable<string>, IDisposable
{
  LazyEnumerator le;

  public LazyStream(FileInfo file, Encoding encoding)
  {
    le = new LazyEnumerator(file, encoding);
  }

  #region IEnumerable<string> Members
  public IEnumerator<string> GetEnumerator()
  {
    return le;
  }
  #endregion

  #region IEnumerable Members
  System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
  {
    return le;
  }
  #endregion

  #region IDisposable Members
  private bool disposed = false;

  public void Dispose()
  {
    Dispose(true);

    GC.SuppressFinalize(this);
  }

  protected virtual void Dispose(bool disposing)
  {
    if (!this.disposed)
    {
      if (disposing)
      {
        if (le != null) le.Dispose();
      }

      disposed = true;
    }
  }
  #endregion

  class LazyEnumerator : IEnumerator<string>, IDisposable
  {
    StreamReader streamReader;
    const int chunksize = 1024;
    char[] buffer = new char[chunksize];

    string current;

    public LazyEnumerator(FileInfo file, Encoding encoding)
    {
      try
      {
        streamReader = new StreamReader(file.OpenRead(), encoding);
      }
      catch
      {
        // Catch some generator related exception
      }
    }

    #region IEnumerator<string> Members
    public string Current
    {
      get { return current; }
    }
    #endregion

    #region IEnumerator Members
    object System.Collections.IEnumerator.Current
    {
      get { return current; }
    }

    public bool MoveNext()
    {
      try
      {
        if (streamReader.Peek() >= 0)
        {
          int readCount = streamReader.Read(buffer, 0, chunksize);

          current = new string(buffer, 0, readCount);

          return true;
        }
        else
        {
          return false;
        }
      }
      catch
      {
        // Trap some iteration error
      }
    }

    public void Reset()
    {
      throw new NotSupportedException();
    }
    #endregion

    #region IDisposable Members
    private bool disposed = false;

    public void Dispose()
    {
      Dispose(true);

      GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
      if (!this.disposed)
      {
        if (disposing)
        {
          if (streamReader != null) streamReader.Dispose();
        }

        disposed = true;
      }
    }
    #endregion
  }
}

I didn't test this, but I think it's close.

used like this:

using (var fe = new LazyStream(new FileInfo("c:\\data.log"), Encoding.ASCII))
{
  foreach (var chunk in fe)
  {
    Console.WriteLine(chunk);
  }
}

EDIT: I had totally forgotten to add the try-catch block placements. Oops.

0
jocull On

One strategy is that effective (if a bit messier to read...) is to break out and wrap each section that might throw around the actual yield return call. This works around the issue so that the yield itself is not in a try/catch block, but the parts that could fail are still contained.

Here's a possible implementation of your code:

StreamReader stream = null;
char[] buffer = new char[chunksize];

try
{
    try
    {
        stream = new StreamReader(file.OpenRead(), Encoding);
    }
    catch (Exception ex)
    {
        throw ExceptionMapper.Map(ex, file.FullName);
    }

    int readCount;
    Func<bool> peek = () =>
    {
        try
        {
            return stream.Peek() >= 0;
        }
        catch (Exception ex)
        {
            throw ExceptionMapper.Map(ex, file.FullName);
        }
    };

    while (peek())
    {
        try
        {
            readCount = stream.Read(buffer, 0, chunksize);
        }
        catch (Exception ex)
        {
            throw ExceptionMapper.Map(ex, file.FullName);
        }

        yield return new string(buffer, 0, readCount);
    }
}
finally
{
    if (stream != null)
    {
        stream.Dispose();
        stream = null;
    }
}
2
Femaref On

You can't use yield constructs in a try/catch block. Restrict the try block to code that can throw, not all of it. If you are unable to do this, you are out of luck - you'll need to catch it further up the stack.

0
Chris Pfohl On

Unfortunately you haven't described what it is you want to do, but you could try just forcing users of the function you're defining to try/catch themselves:

public IEnumerable<string> YourFunction(...)
{
    //Your code
}

//later:
    //...
    try{
        foreach( string s in YourFunction(file) )
        {
            //Do Work
        }
    }
    catch(Exception e){
        throw ExceptionMapper.Map(e, file.FullName);
    }
3
Erik Forbes On

Edit - this answer is actually incorrect, due to the reasons elaborated on in the comments - "ONLY the enumerator generation is wrapped, but not the iteration itself." - but I am leaving this answer here as an example of how sometimes what may appear to work does not due to the intricacies of the language.

Consider it a cautionary tale - my thanks to uosɐſ. =)


Here's an option - separate your method into two methods, one public and one private. The public method is a wrapper (with try/catch) around a call to the private method, which is your generator. For example:

public IEnumerable<string> YourFunction(...)
{
    try
    {
        return _yourFunction(...);
    }
    catch (Exception e)
    {
        throw ExceptionMapper.Map(e, file.FullName);
    }
}

private IEnumerable<string> _yourFunction(...)
{
    // Your code here
}

This will allow your users to rely on the generator having built-in exception handling. Additionally you could perform more validation on your inputs in the public method, throwing any exceptions as needed due to bad inputs, and have those validations performed immediately when the method is called, rather than waiting for the first time the enumerable is enumerated.

0
xtofl On

Take a look at this question. You can yield break in the exceptional case, yield value after the try/catch clause. I was concerned about performance, but there it is believed that try doesn't have a performance influence while no exceptions are thrown.

2
Steve B On

try this approach :

public IEnumerable<ReturnData> toto()
{
    using (StreamReader stream = new StreamReader(File.OpenRead(""), Encoding.UTF8))
    {
        char[] buffer = new char[1];
        while (stream.Peek() >= 0)
        {
            ReturnData result;
            try
            {
                int readCount = stream.Read(buffer, 0, 1);
                result = new ReturnData(new string(buffer, 0, readCount));
            }
            catch (Exception exc)
            {
                result = new ReturnData(exc);
            }
            yield return result;
        }
    }
}

public class ReturnData
{
    public string Data { get; private set; }
    public Exception Error { get; private set; }
    public bool HasError { get { return Error != null; } }
    public ReturnData(string data)
    {
        this.Data = data;
    }
    public ReturnData(Exception exc)
    {
        this.Error = exc;
    }
}

You just have to be careful with this approach: you will have to filter exceptions based on the severity. Some exceptions will have to stop the whole process, others just can be skipped and logged.

0
Thomas On

Here is a code snippet, which works for me (I did not reach the error condition).

while (true)
{
    T ret = null;
    try
    {
        if (!enumerator.MoveNext())
        {
            break;
        }
        ret = enumerator.Current;
    }
    catch (Exception ex)
    {
        // handle the exception and end the iteration
        // probably you want it to re-throw it
        break;
    }
    // the yield statement is outside the try catch block
    yield return ret;
}
1
Keith On

Try using a local function within the enumerator method: move the contents of the try..catch to the local function, then call the function from within the try..catch.

Using your example:

public IEnumerable<string> YourFunction()
{
    // do stuff...

    try
    {
       // Move the try..catch content to the local function
       return getStrings()
    }
    catch (Exception ex)
    {
        throw ExceptionMapper.Map(ex, file.FullName)
    }

    // The local function
    IEnumerable<string> getStrings()
    {
       using (StreamReader stream = new StreamReader(file.OpenRead(), Encoding))
       {
           char[] buffer = new char[chunksize];
           while (stream.Peek() >= 0)
           {
              int readCount = stream.Read(buffer, 0, chunksize);

              yield return new string(buffer, 0, readCount);
           }
        }
    }

}

Using a local function is actually a good idea in a lot of cases. Using this pattern can force the method to validate arguments immediately instead of waiting until the caller begins enumeration (Roslyn warning RCS1227).

0
Theodor Zoulias On

You could consider using the advanced LINQ functionality of the System.Interactive package, and specifically the Catch and Throw operators. Just move your existing code in a local function, and apply the Catch operator on the result of the local function:

public IEnumerable<string> ReadPieces(int pieces)
{
    return Implementation(pieces)
        .Catch<string, Exception>(ex => EnumerableEx
            .Throw<string>(ExceptionMapper.Map(ex, file.FullName)));

    static IEnumerable<string> Implementation(int pieces)
    {
        /* Your existing code, without try/catch */
    }
}

The signature of the Catch operator:

// Creates a sequence that corresponds to the source sequence,
// concatenating it with the sequence resulting from calling
// an exception handler function in case of an error.
public static IEnumerable<TSource> Catch<TSource, TException>(
    this IEnumerable<TSource> source,
    Func<TException, IEnumerable<TSource>> handler)
    where TException : Exception;

The signature of the Throw operator:

// Returns a sequence that throws an exception upon enumeration.
public static IEnumerable<TResult> Throw<TResult>(Exception exception);

The above code will not compile, because the file variable does not exist in the context of the handler delegate. In case the file must be constructed in a deferred manner, you can use the Defer operator:

public IEnumerable<string> ReadPieces(int pieces)
{
    return EnumerableEx.Defer(() =>
    {
        FileInfo file = /* Initialize the variable */
        return Implementation(pieces, file)
            .Catch<string, Exception>(ex => EnumerableEx
                .Throw<string>(ExceptionMapper.Map(ex, file.FullName)));
    });

    static IEnumerable<string> Implementation(int pieces, FileInfo file)
    {
        /* Your existing code, without try/catch */
    }
}

In case you want to suppress the exception (instead of mapping it to a different exception), do this:

    .Catch<string, Exception>(ex => Enumerable.Empty<string>());

In case your sequence is asynchronous (IAsyncEnumerable<string>), you can find identical Catch, Throw and Defer operators for asynchronous sequences in the System.Interactive.Async package.

1
drzaus On

Another consideration -- if you're consuming an IEnumerable method implementing yield that internally throws an exception, you can't catch that individual error and continue enumerating -- see the "Exception Handling" section of https://msdn.microsoft.com/en-us/library/9k7k7cf0.aspx

example:

void Main()
{
    // even is okay, odd will cause exception
    var operations = new[] { 2, 16, 5 /* ! */, 8, 91 /* ! */ };

    var results = process(operations);
    var en = results.GetEnumerator();

    "Regular Enumeration".Title();
    testEnumeration(en);

    results = process(operations, ex => log("Handled: {0}", ex.Message));
    en = results.GetEnumerator();   

    "Handled Exceptions".Title();
    testEnumeration(en);

    results = process(operations, ex => log("Handled+: {0}", ex.Message), true);
    en = results.GetEnumerator();   

    "Handled Exceptions and Continue".Title();
    testEnumeration(en);
}

/// run the test and debug results
void testEnumeration(IEnumerator en) {
    int successCount = 0, failCount = 0;
    bool keepGoing = false;
    do {
        try {
            log("==={0}===", "before next");
            keepGoing = en.MoveNext();
            log("==={0}=== (keepGoing={1}, curr={2})", "after next", keepGoing, en.Current);

            // did we have anything?
            if(keepGoing) {
                var curr = en.Current;
                log("==={0}===", "after curr");

                log("We did it? {0}", curr);
                successCount++;
            }
        }
        catch(InvalidOperationException iopex) {
            log(iopex.Message);
            failCount++;
        }
    } while(keepGoing);

    log("Successes={0}, Fails={1}", successCount, failCount);
}

/// enumerable that will stop completely on errors
IEnumerable<int> process(IEnumerable<int> stuff) {
    foreach(var thing in stuff) {
        if(thing % 2 == 1) {
            throw new InvalidOperationException("Aww, you broked it");
        }

        yield return thing;
    }
}
/// enumerable that can yield from exceptions
IEnumerable<int> process(IEnumerable<int> stuff, Action<Exception> handleException, bool yieldOnExceptions = false) {
    bool shouldYield = false;
    foreach(var thing in stuff) {
        var result = thing;
        try {
            if(thing % 2 == 1) {
                throw new InvalidOperationException("Aww, you broked it");
            }

            shouldYield = true;
        }
        catch(Exception ex) {
            handleException(ex);
            // `yield break` to stop loop
            shouldYield = yieldOnExceptions;
            if(yieldOnExceptions) result = -1; // so it returns a different result you could interpret differently
        }
        if(shouldYield) yield return result;
    }
}

void log(string message, params object[] tokens) {
    Console.WriteLine(message, tokens);
}

results in

    Regular Enumeration    

--------------------------- 
===before next===
===after next=== (keepGoing=True, curr=2)
===after curr===
We did it? 2
===before next===
===after next=== (keepGoing=True, curr=16)
===after curr===
We did it? 16
===before next===
Aww, you broked it
===before next===
===after next=== (keepGoing=False, curr=16)
Successes=2, Fails=1


    Handled Exceptions    

-------------------------- 
===before next===
===after next=== (keepGoing=True, curr=2)
===after curr===
We did it? 2
===before next===
===after next=== (keepGoing=True, curr=16)
===after curr===
We did it? 16
===before next===
Handled: Aww, you broked it
===after next=== (keepGoing=True, curr=8)
===after curr===
We did it? 8
===before next===
Handled: Aww, you broked it
===after next=== (keepGoing=False, curr=8)
Successes=3, Fails=0


    Handled Exceptions and Continue    

--------------------------------------- 
===before next===
===after next=== (keepGoing=True, curr=2)
===after curr===
We did it? 2
===before next===
===after next=== (keepGoing=True, curr=16)
===after curr===
We did it? 16
===before next===
Handled+: Aww, you broked it
===after next=== (keepGoing=True, curr=-1)
===after curr===
We did it? -1
===before next===
===after next=== (keepGoing=True, curr=8)
===after curr===
We did it? 8
===before next===
Handled+: Aww, you broked it
===after next=== (keepGoing=True, curr=-1)
===after curr===
We did it? -1
===before next===
===after next=== (keepGoing=False, curr=-1)
Successes=5, Fails=0

Note that the enumerator's Current is "stuck" on the last successful MoveNext during "regular enumeration", whereas the handled exceptions allows it to complete the loop.