await Task.WhenAll(tasks) Exception Handling, log all exceptions from the tasks

You've fallen foul of lazy evaluation - the result of Select will create a new set of tasks each time you iterate over it. You can fix this just by calling ToList():

var tasks = _factory.CreateMessage(settings)
                    .Select(msg => SendScans(msg))
                    .ToList();

That way the set of tasks that you're awaiting will be the same set of tasks checked with your foreach loop.


Instead of iterating over all tasks, you can get the Exceptions (if any) from the Task.WhenAll-Task:

var taskResult = Task.WhenAll(tasks);
try
{
    await taskResult;
}
catch (Exception e)
{
    if (taskResult.IsCanceled)
    {
        // Cancellation is most likely due to a shared cancellation token. Handle as needed, possibly check if ((TaskCanceledException)e).CancellationToken == token etc.       
    }
    else if (taskResult.IsFaulted)
    {
        // use taskResult.Exception which is an AggregateException - which you can iterate over (it's a tree! .Flatten() might help)
        // caught exception is only the first observed exception
    }
    else
    {
        // Well, this should not really happen because it would mean: Exception thrown, not faulted nor cancelled but completed
    }
}