Mezclando flujos síncronos y asíncronos usando promesas en JavaScript

Manejar flujos de datos síncronos es fácil, con Async/Await también es fácil hacerlos asíncronos. Pero qué pasa si tenemos una función que debe realizar cierta lógica síncrona y luego asíncrona y devolver una promesa. Si esta lógica síncrona falla, ¿qué ocurriría?

Si todo está bien obtenemos una promesa que se resuelve con el resultado, si hay un error en la parte asíncrona obtenemos una promesa que se rechaza y vemos el error. Pero si el error ocurre en la parte síncrona entonces obtenemos un error normal y no una promesa.

const promesa = readMultiFiles();

// si promesa da un error síncrono todo lo de abajo se rompe
promesa.then(data => console.log(data)).catch(error => console.error(error));

Vamos a ver como podemos evitar esto y mezclar estos dos flujos síncronos y asíncronos. Para eso vamos a crear una función que recibe un string con una lista de rutas de archivos, vamos a convertir ese string en un array y luego leer todos esos archivos del disco.

import fs from 'fs'
import { promisify } from 'utils'

const readFile = promisify(fs.readFile);

const readMultiFiles = list => {
  const files = JSON.parse(list)
  return Promise.all(
    files.map(file => readFile(file, 'utf8')
  )
}

Esa es nuestra función, ese JSON.parse se puede romper si mandamos un dato inválido en el parámetro list. Vamos a meter entonces un try/catch.

import fs from 'fs'
import { promisify } from 'utils'

const readFile = promisify(fs.readFile);

const readMultiFiles = list => {
  try {
    const files = JSON.parse(list)
    return Promise.all(
      files.map(file => readFile(file, 'utf8')
    )
  } catch (error) {
    // hacemos algo con el error
  }
}

Por ahora no hicimos nada con el error. Lo que vamos a hacer es usar un método estático del objeto Promise para crear una promesa que ya este rechazada. Eso se hace gracias a Promise.reject a la cual le pasamos un objeto error y nos devuelve una promesa automáticamente rechazada.

import fs from 'fs'
import { promisify } from 'utils'

const readFile = promisify(fs.readFile);

const readMultiFiles = list => {
  try {
    const files = JSON.parse(list)
    return Promise.all(
      files.map(file => readFile(file, 'utf8')
    )
  } catch (error) {
    return Promise.reject(error)
  }
}

¡Con eso si ocurre un error síncrono nuestra función va a igualmente devolver una promesa! Genial, ahora vamos a ver algo: ¿Qué pasa si list contiene un objeto? Si el valor es por ejemplo "{}" entonces no va a dar error al hacer el JSON.parse. Podemos validar esto con una condición y devolver un error personalizado.

import fs from 'fs'
import { promisify } from 'utils'

const readFile = promisify(fs.readFile);

const readMultiFiles = list => {
  try {
    const files = JSON.parse(list)
    if (!Array.isArray(files)) throw new TypeError('The list of files must be a list not a map.')
    return Promise.all(
      files.map(file => readFile(file, 'utf8')
    )
  } catch (error) {
    return Promise.reject(error)
  }
}

Luego podemos agregar otra validación para que si files está vacío directamente, devolvemos una promesa resuelta con una lista vacía. Eso lo hacemos con otro método estático Promise.resolve que crea una promesa resuelta con los datos que le hayamos pasado.

import fs from 'fs'
import { promisify } from 'utils'

const readFile = promisify(fs.readFile);

const readMultiFiles = list => {
  try {
    const files = JSON.parse(list)
    if (!Array.isArray(files)) throw new TypeError('The `list` argument must be a list not a map.')
    if (files.length === 0) return Promise.resolve([])
    return Promise.all(
      files.map(file => readFile(file, 'utf8')
    )
  } catch (error) {
    return Promise.reject(error)
  }
}

Con eso acabamos de crear una función que mezcla flujos síncronos y asíncronos, hace validaciones para devolver algunos errores personalizados o terminar todo .

Conclusiones

Como vemos gracias a los métodos estáticos Promise.resolve y Promise.reject es muy fácil mezclar flujos síncronos y asíncronos y asegurarnos de devolver siempre promesas y no tener que estar validando tipos de datos.