synchronous programming
asynchronous programming
code safety
concurrency
software development

How to safely mix sync and async code?

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

In modern programming, it's common to encounter scenarios where both synchronous (sync) and asynchronous (async) code must coexist. This combination can be tricky, especially when it comes to guaranteeing safe execution and avoiding potential pitfalls like deadlocks or uncaught exceptions. This article will explore how to effectively and safely combine sync and async code, with technical explanations and practical examples.

Understanding Sync and Async Code

Before diving into the integration of sync and async code, it's essential to understand the fundamental differences:

  • Synchronous Code:
    • Operations are executed sequentially.
    • Each operation blocks the execution of the next until it completes.
    • Easier to reason about but can lead to performance bottlenecks.
  • Asynchronous Code:
    • Operations can occur out of order.
    • Allows other operations to execute while waiting for the asynchronous operation to complete.
    • Improves performance by not blocking the main thread but can lead to complex code and tricky bugs.

Common Pitfalls

When mixing synchronous and asynchronous code, developers often encounter several challenges:

  1. Deadlocks:
    • Occur when synchronous code waits indefinitely for an asynchronous task to complete.
    • Often results from blocking the main thread, waiting for an async operation.
  2. Exceptions:
    • Async code might throw exceptions that aren't caught by the sync context, leading to crashes.
  3. Race Conditions:
    • Asynchronous code can introduce race conditions where two operations try to modify the same data concurrently, leading to undefined behavior.

Best Practices

Implementing best practices when mixing sync and async code can minimize risks:

1. Avoid Blocking Calls Waiting for Async Tasks

Blocking calls are a common source of deadlocks and should be avoided. For example, substituting Task.Wait() with await in C# can mitigate the risk:

csharp
1// Blocking call
2task.Wait();
3
4// Recommended non-blocking call
5await task;

2. Use .ConfigureAwait(false)

In library code that doesn’t need to synchronize with the caller's context, use .ConfigureAwait(false) to improve performance and avoid deadlocks:

csharp
await asyncOperation.ConfigureAwait(false);

3. Proper Exception Handling

Since async methods return tasks, exceptions should be handled in task continuation or with try-catch blocks:

csharp
1try
2{
3    await asyncOperation();
4}
5catch (Exception ex)
6{
7    // Handle exceptions accordingly
8}

4. Be Careful with .Result and .GetAwaiter().GetResult()

Directly accessing the result of an asynchronous operation using .Result or .GetAwaiter().GetResult() can cause deadlocks, especially on UI threads:

csharp
1// Potentially blocking call
2var result = asyncOperation.Result;
3
4// Safe alternative
5var result = await asyncOperation;

Examples

Safe Integration in C#

csharp
1public async Task ProcessDataAsync()
2{
3    var result = await GetDataFromServiceAsync();
4    ProcessData(result);
5}
6
7// Do not call the async method from sync context like Main method
8public static void Main(string[] args)
9{
10    // Safe approach by using an async context
11    var dataProcessor = new DataProcessor();
12    dataProcessor.ProcessDataAsync().Wait();
13}

Safe Integration in Node.js

Node.js handles async calls out-of-the-box with promises and async/await, but ensuring appropriate exception handling is still critical:

javascript
1async function fetchData() {
2  try {
3    let response = await fetch("api/data");
4    let data = await response.json();
5    processData(data);
6  } catch (err) {
7    console.error("Error fetching data", err);
8  }
9}
10
11function main() {
12  fetchData()
13    .then(() => console.log("Data processed"))
14    .catch((err) => console.error("Unhandled error", err));
15}
16
17main();

Summary Table

ConsiderationDescription
Avoid Blocking CallsUse await instead of blocking with .Wait() or .Result.
Use .ConfigureAwait()Use .ConfigureAwait(false) in library code.
Handle ExceptionsWrap async calls in try-catch blocks to handle exceptions.
UI Thread CautionAvoid using async methods that directly affect the UI thread in a blocking way.
Asynchronous ContextExecute async code in an async context to prevent deadlocks.

Conclusion

Mixing synchronous and asynchronous code can be a potent method when done correctly, allowing for efficient use of resources and improved user responsiveness. By adhering to the guidelines and best practices detailed above, developers can ensure that their codebase remains maintainable, efficient, and safe from common pitfalls associated with intertwining sync and async operations.


Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.