Guía de async y await en Dart

Dominando async y await en Dart

La palabra clave async es el marcador que le indica al compilador que una función operará de forma asíncrona; su efecto inmediato es que, aunque la función parezca devolver un tipo T, en realidad siempre devolverá un Future<T>. Por su parte, await es el operador que pausa la ejecución de la función actual hasta que el Future en cuestión se complete, extrayendo su valor directamente. No bloqueas el hilo de ejecución (el event loop sigue trabajando), sino que suspendes la micro-tarea de esa función específica.

Este modelo existe porque la ejecución asíncrona permite que el programa siga respondiendo mientras se espera una respuesta de red o de disco. Si intentaras hacer esto de forma síncrona, todo el proceso se congelaría. Usamos async/await cuando necesitamos que el flujo de lógica dependa del resultado de una operación asíncrona (como leer un archivo y luego procesarlo). Si no usas await correctamente, lo que obtendrás no será el dato esperado, sino el objeto Future en sí mismo, lo que suele romper la lógica de tipos o la ejecución posterior.

Para usar await, debes estar dentro de una función marcada con async, una regla que suele llamarse “contagio asíncrono”: la asincronía se propaga hacia arriba en la pila de llamadas. Además, si una función async no devuelve un valor útil, el tipo de retorno debe ser Future<void>.

import 'dart:async';

// Simulamos una llamada a una API externa
Future<String> fetchUserData(int userId) async {
  // Simulamos latencia de red
  await Future.delayed(const Duration(milliseconds: 800));

  if (userId == 0) {
    throw Exception('ID de usuario inválido');
  }

  return 'Datos de usuario para ID: $userId';
}

Future<void> processUsersSequentially(List<int> ids) async {
  print('--- Iniciando proceso secuencial ---');
  for (final id in ids) {
    try {
      // Cada await espera a que termine el anterior
      final data = await fetchUserData(id);
      print('Secuencial: $data');
    } catch (e) {
      print('Error en proceso secuencial con ID $id: $e');
    }
  }
}

Future<void> processUsersInParallel(List<int> ids) async {
  print('\n--- Iniciando proceso en paralelo ---');
  
  // Creamos una lista de Futures, pero NO los estamos esperando todavía
  final List<Future<String>> futures = ids.map((id) => fetchUserData(id)).toList();

  try {
    // Future.wait lanza una excepción si cualquiera de los Futures falla
    final results = await Future.wait(futures);
    
    for (final result in results) {
      print('Paralelo: $result');
    }
  } catch (e) {
    print('Error en proceso paralelo: $e');
  }
}

Future<void> main() async {
  final userIds = [1, 2, 0, 4];

  // Ejecución secuencial: toma ~3.2 segundos (4 * 800ms)
  await processUsersSequentially(userIds);

  // Ejecución en paralelo: toma ~800ms (todos corren a la vez)
  await processUsersInParallel(userIds);
  
  print('\nProcesos completados.');
}

En el ejemplo anterior, fetchUserData es una función async que devuelve un Future<String>. Internamente, el compilador de Dart transforma esta función en una máquina de estados. Cuando se encuentra el await Future.delayed, la función se “suspende”, liberando el hilo, y la máquina de estados se guarda para retomarse cuando el temporizador dispare el evento.

En processUsersSequentially, el uso de await dentro de un bucle for obliga a que cada iteración espere a que la promesa anterior se resuelva. Esto es útil cuando la segunda petición depende de la primera, pero es ineficiente si las tareas son independientes. En cambio, en processUsersInParallel, primero generamos una lista de Futures mediante map. En este punto, las operaciones ya han comenzado a ejecutarse en segundo plano. Al llamar a await Future.wait(futures), le decimos a Dart: “espera a que todas estas tareas terminen; si todas tienen éxito, dame una lista con los resultados; si una falla, lánzame un error”. Esta es la forma correcta de maximizar el rendimiento en operaciones de I/O.

Finalmente, nota que main también es async. En Dart moderno, el punto de entrada main puede ser asíncrono, lo que nos permite usar await directamente en el nivel superior del programa para orquestar la ejecución inicial.

El error frecuente
Un error común es el “fire-and-forget” accidental: llamar a una función async sin usar await y sin capturar su error.

void performRiskyTask() async {
  await Future.delayed(const Duration(seconds: 1));
  throw Exception('Falla catastrófica');
}

void main() {
  // ERROR: No estamos usando 'await' ni capturando el error.
  // La función se lanza en segundo plano, la ejecución de main sigue,
  // y cuando la excepción ocurra, no habrá ningún try/catch que la atrape.
  performRiskyTask(); 
  
  print('Esto se imprimirá antes de que la tarea falle.');
}

En este escenario, la excepción se convierte en un “unhandled exception”, lo cual puede causar que el proceso termine inesperadamente o que sea muy difícil de rastrear en logs de producción. Si realmente quieres ejecutar algo en segundo plano sin esperar su resultado, asegúrate de que la función asíncrona misma contenga su propio bloque try/catch para gestionar sus propios errores.

66

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio