Revisiting Exception Handling in async methods

It is interesting to observe how the Exceptions behave in async code. While the .Net framework attempts to ensure the exprerience of handling failures in async methods are as similar to the synchronous methods, there are subtle differences which is worth understanding.

Let us examine the following code.

async Task<string> Foo()
{
	var client = new HttpClient();
	try
	{
		return await client.GetStringAsync("http://www.InvalidUrl.com");
	}
	catch(HttpRequestException ex)
	{
		Console.WriteLine($"Exception of Type {ex.GetType()} has been raised");
	}
	catch(AggregateException ex)
	{
		Console.WriteLine($"Exception of Type {ex.GetType()} has been raised");
	}
	catch(Exception ex)
	{
		Console.WriteLine($"Exception of Type {ex.GetType()} has been raised");
	}
	return default;
}

What could be the output of the above code ? Specifically, what type of an exception would be caught assuming the Url is invalid ?

Before we get to the answer, let us examine how the returned Task<string> object indicate the failure.

Property/MethodIndication
StatusFaulted
IsFaultedtrue
ExceptionAggregateException
Wait()Throws AggregateException
ResultThrows AggregateException
await TaskThrows First Exception with AggregateException

The last 3 rows holds importance to our question. Let us reexamnine the method call.

return client.GetStringAsync("http://www.InvalidUrl.com").

In the above case, we would recieve an AggregateException with an HttpRequestException within it.

When Exception causes Status |= Faulted

As mentioned above, the status of Task would be set to Faulted in most cases, except for one particular kind of Exception,the OperationCancelledException.

Let us write some code before we discuss this further.

async void Main()
{
	var task = Foo();
	try
	{
		await task;
	}
	catch(Exception Ex)
	{
		Console.WriteLine($"Task Status:{task.Status}, Exception:{Ex.Message}");
	}
}

async Task Foo()
{
	throw new OperationCanceledException();
}

Examining the output

Task Status:Canceled, Exception:The operation was canceled.

The TPL uses OperationCanceledException when a Token from CancelationTokenSource is canceled by the calling method. If a method like the code above, decides to throw this special exception, then instead of the Status being set to Faulted, the Status is set to Canceled.

Lazy Exceptions

There is another aspect of Exception Handling in async methods that are worth examining. An async task would not directly throw an exception, instead it would return a faulted Task. Significance of the method could be better understood with help of a bit of code.

async void Main()
{
	try
	{
		var fooTask = Foo(-3);
		Console.WriteLine("Task is not awaited yet");
		await fooTask;
		Console.WriteLine("Task Completed");
	}
	catch(ArgumentException ex)
	{
		Console.WriteLine($"{nameof(ArgumentException)} has been raised");
	}
}


public async Task<string> Foo(int value)
{
	Console.WriteLine($"Method {nameof(Foo)} Invoked");
	if(value<0)
	{
		throw new ArgumentException();
	}
	
	Console.WriteLine($"Method {nameof(Foo)} mocking real task via Delay");
	await Task.Delay(1000);
	return default;
}

The Foo() method has a precondition check which validates if the passed arguement is a positive number. Otherwise, it raises an ArgumentException. With the example code, the method invoker is passing a negative value to the method, and should hit the precondition block.

Let us examine the output and discuss further.

Method Foo Invoked
Task is not awaited yet
ArgumentException has been raised

As you can observe, the message “Task is not awaited yet” is displayed before the exception thrown. This is because exceptions would not be raised untill the task is awaited (or completed). This lazy nature of evaluation of exceptions could be useful at most times, but in times such as above, where preconditions needs to be evaluated and the developer would prefer an early evaluation, this would need a slight workaround.

The idea, similar to how we made iterator methods to evalute exceptions early (and as John Skeets mentions in his invaluable book series C# in Depth), lies in introducing a synchronous method which does the arguement validation, and which in-turn calls the original method. If the original method is moved as an internal method of proposed method, the original method can now safely assume that the arguements are validated.

public Task<string> Foo(int value)
{
	Console.WriteLine($"Method {nameof(Foo)} Invoked");
	if(value<0)
	{
		throw new ArgumentException();
	}
	
	async Task<string> FooAsync()
	{
		Console.WriteLine($"Method {nameof(Foo)} mocking real task via Delay");
		await Task.Delay(1000);
		return default;
	}
	return FooAsync();
}

This ensures the validation and subsequent exception is evaluated early. Let’s hit F5 and run our code now.

Method Foo Invoked
ArgumentException has been raised

As observed, the exception has been evaluated early and we get the much desired result.

That’s all for now, see you soon again

One thought on “Revisiting Exception Handling in async methods

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s