DEV Community

Ge Ji
Ge Ji

Posted on

Dart Lesson 13: Asynchronous Programming Basics — Future and async/await

In previous lessons, almost all the code we wrote executed synchronously – code runs from top to bottom, with each line completing before the next one starts. But in real-world development, many operations (like network requests or file I/O) need to wait for external responses. Handling these with synchronous code would cause our programs to "freeze." Today we'll learn about asynchronous programming – a core technology for solving these problems, which is especially crucial in UI applications (like Flutter apps).

I. Synchronous vs Asynchronous: Why UI Programs Need Can't Live Without Asynchronous Programming

1. Limitations of Synchronous Execution

Synchronous code flows in a straight line, each step must wait for the previous one to complete:

void main() {
  print("Starting execution");
  print("Performing time-consuming operation...");
  // Simulate a 3-second operation (like a network request)
  for (int i = 0; i < 1000000000; i++) {}
  print("Operation completed");
  print("Continuing with other tasks");
}

// Output order:
// Starting execution
// Performing time-consuming operation...
// (3-second wait)
// Operation completed
// Continuing with other tasks
Enter fullscreen mode Exit fullscreen mode

This pattern causes serious problems with time-consuming operations (like network requests or large data processing):

  • The entire program becomes "blocked" and can't respond to user actions (like button clicks or screen swipes)
  • The UI freezes, creating a poor "unresponsive" user experience

2. Advantages of Asynchronous Execution

The core of asynchronous code is: Time-consuming operations run in the "background" while the main thread continues processing other tasks, with results handled once the operation completes.

A real-life analogy:

  • Synchronous: Staring at a kettle while waiting for water to boil, doing nothing else until it's ready.
  • Asynchronous: Preparing tea leaves and cups while waiting for water to boil, then returning when it's ready.

In UI applications, asynchronous programming is essential:

  • Keeps the UI thread unblocked, ensuring it can always respond to user actions
  • Improves program efficiency by allowing multiple tasks to be processed "in parallel"

II. Future: The "Placeholder" for Asynchronous Operations

In Dart, a Future represents an "operation that will complete in the future." It's a placeholder for an asynchronous operation – we don't know the result when we create it, but we know we'll get one eventually (either a success or failure).

1. Three States of a Future

  • Pending: The asynchronous operation is in progress, no result yet
  • Completed with value: The asynchronous operation succeeded, returning a result
  • Completed with error: The asynchronous operation failed, returning an error

2. Creating Futures and Handling Results

Create asynchronous operations with the Future constructor, and handle success with then, errors with catchError:

void main() {
  print("Starting main thread tasks");

  // Create a Future (asynchronous operation)
  Future<String> fetchData() {
    // Simulate network request returning after 2 seconds
    return Future.delayed(Duration(seconds: 2), () {
      // Simulate success scenario
      return "Fetched data: Dart Asynchronous Programming";

      // Simulate failure scenario (comment out above line and uncomment below)
      // throw Exception("Network error: failed to fetch data");
    });
  }

  // Call the async function to get a Future object
  Future<String> future = fetchData();

  // Register callback: executes when Future completes successfully
  future
      .then((data) {
        print("Async operation succeeded: $data");
      })
      .catchError((error) {
        // Register callback: executes when Future fails
        print("Async operation failed: ${error.toString()}");
      })
      .whenComplete(() {
        // Register callback: always executes whether success or failure
        print("Async operation finished (success or failure)");
      });

  print("Main thread continues with other tasks");
}

// Output order (success scenario):
// Starting main thread tasks
// Main thread continues with other tasks
// (2-second wait)
// Async operation succeeded: Fetched data: Dart Asynchronous Programming
// Async operation finished (success or failure)
Enter fullscreen mode Exit fullscreen mode

Key characteristics:

  • After calling fetchData(), the main thread doesn't wait but immediately executes the next print
  • The callback in then executes only after the async operation completes after 2 seconds
  • catchError captures errors thrown in the async operation
  • whenComplete is similar to "finally" and executes regardless of success or failure

3. Future.value and Future.error

Quickly create "already completed" Futures:

void main() {
  // Create a successfully completed Future directly
  Future.value("Direct success result").then((data) {
    print(data); // Output: Direct success result
  });

  // Create a failed Future directly
  Future.error(Exception("Direct error")).catchError((error) {
    print(error); // Output: Exception: Direct error
  });
}
Enter fullscreen mode Exit fullscreen mode

III. async/await: Writing Asynchronous Code with Synchronous Style (Key Focus)

While then chaining can handle asynchronous operations, multiple levels of nesting lead to "callback hell" (bloated, hard-to-maintain code). Dart provides async/await syntactic sugar that lets us write asynchronous logic with synchronous-style code.

1. Basic Usage

  • async: Modifies a function, indicating it's asynchronous, with its return value automatically wrapped in a Future
  • await: Can only be used inside async functions, waits for a Future to complete and retrieves its result
void main() {
  print("Starting main thread tasks");

  // Call async function (main thread continues without waiting)
  fetchAndPrintData();

  print("Main thread continues with other tasks");
}

// Modified with async, indicating this is an asynchronous function
Future<void> fetchAndPrintData() async {
  try {
    print("Starting async data fetch");

    // Use await to wait for Future completion and get result (synchronous-style)
    String data = await fetchData(); // Waits for fetchData() to complete

    // The await above "pauses" the function until the Future completes
    print("Async operation succeeded: $data");
  } catch (error) {
    // Catches errors in async operations (replaces catchError)
    print("Async operation failed: ${error.toString()}");
  } finally {
    // Executes regardless of success or failure (replaces whenComplete)
    print("Async operation finished (success or failure)");
  }
}

// Async function simulating network request
Future<String> fetchData() {
  return Future.delayed(Duration(seconds: 2), () {
    return "Fetched data: async/await is convenient";
    // Simulate failure: throw Exception("Network error");
  });
}

// Output order:
// Starting main thread tasks
// Main thread continues with other tasks
// Starting async data fetch
// (2-second wait)
// Async operation succeeded: Fetched data: async/await is convenient
// Async operation finished (success or failure)
Enter fullscreen mode Exit fullscreen mode

2. Why Do async Functions Return Futures?

  • Functions modified with async have their return values automatically wrapped in a Future
  • If a function returns int, its actual return type is Future
  • If a function has no return value, its actual return type is Future
// Returns Future<int>
Future<int> calculate() async {
  await Future.delayed(Duration(seconds: 1));
  return 100; // Automatically wrapped as Future.value(100)
}

void main() async {
  int result = await calculate(); // Use await to get the result
  print(result); // Output: 100
}
Enter fullscreen mode Exit fullscreen mode

3. Handling Multiple Asynchronous Operations

async/await simplifies sequential execution of multiple asynchronous operations:

// Simulate three async operations
Future<String> fetchUser() =>
    Future.delayed(Duration(seconds: 1), () => "User information");
Future<String> fetchOrders() =>
    Future.delayed(Duration(seconds: 1), () => "Order list");
Future<String> fetchRecommendations() =>
    Future.delayed(Duration(seconds: 1), () => "Recommended products");

// Execute multiple async operations sequentially
Future<void> fetchAllData() async {
  print("Starting to fetch all data...");

  // Execute sequentially, total time ~3 seconds
  String user = await fetchUser();
  print("Fetched: $user");

  String orders = await fetchOrders();
  print("Fetched: $orders");

  String recommendations = await fetchRecommendations();
  print("Fetched: $recommendations");

  print("All data fetching completed");
}

void main() {
  fetchAllData();
}
Enter fullscreen mode Exit fullscreen mode

For independent async operations, execute them in parallel (with Future.wait):

Future<void> fetchAllDataParallel() async {
  print("Starting parallel fetch of all data...");

  // Execute in parallel, total time ~1 second (takes longest single operation time)
  List<Future<String>> futures = [
    fetchUser(),
    fetchOrders(),
    fetchRecommendations(),
  ];

  // Wait for all Futures to complete, returns list of results
  List<String> results = await Future.wait(futures);

  for (String result in results) {
    print("Fetched: $result");
  }

  print("All data fetching completed");
}
Enter fullscreen mode Exit fullscreen mode

IV. Common Asynchronous Programming Pitfalls

1. Forgetting to Use await

Future<int> getNumber() async => 42;

void main() async {
  // Error: Forgot to use await, getting Future instead of result
  var result = getNumber();
  print(result); // Output: Instance of 'Future<int>'

  // Correct: Use await to get result
  var correctResult = await getNumber();
  print(correctResult); // Output: 42
}
Enter fullscreen mode Exit fullscreen mode

2. Using await in Non-async Functions

// Error: await can only be used in async functions
void badFunction() {
  // await Future.delayed(Duration(seconds: 1)); // Compile error
}

// Correct: Modify function with async
void goodFunction() async {
  await Future.delayed(Duration(seconds: 1)); // Correct
}
Enter fullscreen mode Exit fullscreen mode

3. Unhandled Exceptions Causing Crashes

Future<void> riskyOperation() async {
  throw Exception("Unexpected error"); // Throw exception
}

void main() async {
  // Error: Unhandled exception will crash the program
  // await riskyOperation();

  // Correct: Handle exception with try-catch
  try {
    await riskyOperation();
  } catch (e) {
    print("Caught exception: $e"); // Safe handling
  }
}
Enter fullscreen mode Exit fullscreen mode

V. Practical Application Scenarios

Asynchronous programming is everywhere in real-world development:

Network requests:

Future<User> fetchUserInfo(String userId) async {
  final response = await http.get(
    Uri.parse("https://api.example.com/users/$userId"),
  );
  if (response.statusCode == 200) {
    return User.fromJson(json.decode(response.body));
  } else {
    throw Exception("Failed to load user");
  }
}
Enter fullscreen mode Exit fullscreen mode

Local storage operations:

Future<void> saveData(String key, String value) async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.setString(key, value);
}
Enter fullscreen mode Exit fullscreen mode

Delayed execution:

Future<void> showSplashScreen() async {
  print("Showing splash screen");
  await Future.delayed(Duration(seconds: 3)); // Wait 3 seconds
  print("Closing splash screen, entering home page");
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)