Search code examples
c#async-awaittask-parallel-librarywait

.Wait() causes TaskCanceledException


I have a function that sends out an email, like so:

public async Task SendEmail(string from, string to, string subject, string body, bool isBodyHtml = false)
        {
            await Task.Run(() =>
            {
                using (SmtpClient smtp = new SmtpClient(host, port))
                {
                    smtp.Credentials = new NetworkCredential(userName, password);
                    smtp.EnableSsl = true;
                    smtp.SendCompleted += SmtpOnSendCompleted;
                    MailMessage message = new MailMessage(from, to, subject, body);
                    message.IsBodyHtml = isBodyHtml;
                    smtp.Send(message);
                }
            }).ContinueWith(task =>
            {
                LoggingService.Instance.BusinessLogger.Error(task.Exception.Flatten().InnerException);

            }, TaskContinuationOptions.OnlyOnFaulted);
        }

As you can see it is not a "true async", but rather a "deffered execution", so that I can call this method and it wouldn't block the current calling thread.

Now, I sometimes need a way to wait for the email to be sent, before proceeding. So I call my SendMail() method like so:

EmailService.Instance.SendEmail("[email protected]", "[email protected]", "Subject", "Body text").Wait();

with a .Wait() at the end.

For some reason using .Wait() - trying to force synchronous execution, cause exception:

System.Threading.Tasks.TaskCanceledException: A task was canceled

Questions:

1) Why am I getting this exception?

2) How do I force synchronous execution of this method?

Thanks


Solution

  • 1) Why am I getting this exception?

    You are getting the exception because,

    • The original Task completed successfully without any faults
    • You're having a continuation with TaskContinuationOptions set as TaskContinuationOptions.OnlyOnFaulted
    • As there were no faults in the original Task's execution, you get a AggregateException: A task was canceled. because the continuation didn't execute and it was canceled

    2) How do I force synchronous execution of this method?

    You can force synchronous execution by,

    e.g.

    var task = new Task(() => { ... });
    task.RunSynchronously();
    

    Check how the below program behaves when you throw an error in the original Task and when the original Task completes without any faults by commenting/uncommenting the dummy exception. You can execute the below program at http://rextester.com/

    using System;
    using System.Threading.Tasks;
    
    namespace Rextester
    {
        public class Program
        {
            public static void Main(string[] args)
            {
                try
                {
                    DoSomething().Wait();
                }
                catch (AggregateException ex)
                {
                    Console.WriteLine(ex.InnerException.Message);
                }
    
                Console.WriteLine("DoSomething completed");
            }
    
            public static async Task DoSomething()
            {
                await Task.Factory.StartNew(() =>
                {
                    System.Threading.Thread.Sleep(1000);
                    Console.WriteLine("Doing Something");
                    // throw new Exception("Something wen't wrong");
                }).ContinueWith(task =>
                {
                    Console.WriteLine(task.Exception.InnerException.Message);
                }, TaskContinuationOptions.OnlyOnFaulted);
            }
        }
    }
    

    If you're only logging the exception when anything goes wrong using the ContinueWith method, then you can get rid of that ContinueWith and put a try catch block inside the original Task to catch any exceptions and log them.

    static void Main(string[] args)
    {
        DoSomething().Wait();
        Console.WriteLine("DoSomething completed");
        Console.ReadKey();
    }
    
    public static async Task DoSomething()
    {
        await Task.Factory.StartNew(() =>
        {
            try
            {
                System.Threading.Thread.Sleep(1000);
                Console.WriteLine("Doing Something");
                throw new Exception("Something wen't wrong");
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        });
    }
    

    Otherwise, if you want to do some additional work after original Task completes you can do it as follows.

    namespace SO
    {
        using System;
        using System.Threading.Tasks;
    
        class Program
        {
            static void Main(string[] args)
            {
                DoSomething().Wait();
                Console.WriteLine("DoSomething completed");
                Console.ReadKey();
            }
    
            public static async Task DoSomething()
            {
                await Task.Factory.StartNew(() =>
                {
                    System.Threading.Thread.Sleep(1000);
                    Console.WriteLine("Doing Something");
                    // throw new Exception("Something wen't wrong");
                }).ContinueWith(task =>
                {
                    if (task.Status == TaskStatus.Faulted)
                    {
                        // log exception
                        Console.WriteLine(task.Exception.InnerException.Message);
                    }
                    else if (task.Status == TaskStatus.RanToCompletion)
                    {
                        // do continuation work here
                    }
                });
            }
        }
    }