¿Rechazar una promesa solo para casos de error?

25

Digamos que tengo esta función de autenticación que devuelve una promesa. La promesa luego se resuelve con el resultado. Falso y verdadero son los resultados esperados, según lo veo, y los rechazos solo deberían ocurrir en un caso de error. O, ¿una falla en la autenticación se considera algo por lo que rechazaría una promesa?

Mathieu Bertin
fuente
Si la autenticación falla, debe rejecty no devolver falso, pero si espera que el valor sea a Bool, entonces tuvo éxito y debe resolver con el Bool independientemente del valor. Las promesas son una especie de representación de los valores: almacenan el valor devuelto, por lo que solo si el valor no se puede obtener, usted debe hacerlo reject. De lo contrario deberías resolve.
Esta es una buena pregunta. Toca uno de los fracasos del diseño de la promesa. Hay dos tipos de errores, fallas esperadas, como cuando un usuario proporciona una entrada incorrecta (como no iniciar sesión) y fallas inesperadas, que son errores en el código. El diseño prometedor combina los dos conceptos en un solo flujo, lo que hace difícil distinguir los dos para su manejo.
zzzzBov
1
Yo diría que resolver significa usar la respuesta y continuar con su aplicación, mientras que rechazar significa cancelar la operación actual (y posiblemente volver a intentarlo o hacer otra cosa).
44
Otra forma de pensarlo: si se tratara de una llamada al método sincrónico, ¿consideraría que la falla de autenticación regular (nombre de usuario / contraseña incorrecta) es una devolución falseo una excepción?
wrschneider
2
La API Fetch es un buen ejemplo de esto. Siempre se activa thencuando el servidor responde, incluso si se devuelve un código de error, y debe verificar el response.ok. El catchcontrolador solo se activa para errores inesperados .
CodingIntrigue

Respuestas:

22

¡Buena pregunta! No hay una respuesta difícil. Depende de lo que considere excepcional en ese punto específico del flujo .

Rechazar a Promisees lo mismo que plantear una excepción. No todos los resultados no deseados son excepcionales , el resultado de errores . Podría argumentar su caso en ambos sentidos:

  1. Falló la autenticación debe rejectal Promise, debido a que la persona que llama está a la espera de un Userobjeto a cambio, y cualquier otra cosa es una excepción a este flujo.

  2. Falló la autenticación debe resolveal Promise, aunque en null, ya que proporciona las credenciales incorrectas no es realmente un excepcional caso, y la persona que llama debe no espera que el flujo de siempre resulta en un User.

Tenga en cuenta que estoy mirando el problema desde el lado de la persona que llama . En el flujo de información, ¿la persona que llama espera que sus acciones resulten en un User(y cualquier otra cosa es un error), o tiene sentido que esta persona en particular maneje otros resultados?

En un sistema de varias capas, la respuesta puede cambiar a medida que los datos fluyen a través de las capas. Por ejemplo:

  • La capa HTTP dice ¡RESOLVER! Se envió la solicitud, el socket se cerró limpiamente y el servidor emitió una respuesta válida. La API Fetch hace esto.
  • La capa de protocolo luego dice RECHAZAR! El código de estado en la respuesta fue 401, lo cual está bien para HTTP, ¡pero no para el protocolo!
  • La capa de autenticación dice NO, ¡RESUELVE! Detecta el error, ya que 401 es el estado esperado para una contraseña incorrecta y se resuelve para un nullusuario.
  • Controlador de interfaz dice NADA DE ESO, RECHAZAR! El modo que se muestra en la pantalla esperaba un nombre de usuario y un avatar, y cualquier otra cosa que no sea esa información es un error en este momento.

Este ejemplo de 4 puntos es obviamente complicado, pero ilustra 2 puntos:

  1. Si algo es una excepción / rechazo o no depende del flujo circundante y las expectativas
  2. Las diferentes capas de su programa pueden tratar el mismo resultado de manera diferente, ya que se encuentran en diferentes etapas del flujo

Así que de nuevo, no hay respuesta difícil. ¡Hora de pensar y diseñar!

slezica
fuente
6

Por lo tanto, las promesas tienen una buena propiedad que aportan JS de lenguajes funcionales, que es que implementan este Eitherconstructor de tipos que une dos otros tipos, el Lefttipo y el Righttipo, al forzar a la lógica a tomar una rama u otra rama.

data Either x y = Left x | Right y

Ahora se está dando cuenta de que el tipo en el lado izquierdo es ambiguo para las promesas; puedes rechazar con cualquier cosa. Esto es cierto porque JS está tipado débilmente, pero debes ser cauteloso si estás programando a la defensiva.

La razón es que JS tomará throwdeclaraciones del código de manejo de promesas y lo agrupará en el Leftlado de eso también. Técnicamente en JS puedes throwtodo, incluso verdadero / falso o una cadena o un número: pero el código JavaScript también arroja cosas sinthrow (cuando haces cosas como intentar acceder a propiedades en nulos) y hay una API establecida para esto (el Errorobjeto) . Entonces, cuando empiezas a atrapar, generalmente es bueno poder asumir que esos errores son Errorobjetos. Y dado rejectque la promesa se aglomerará en cualquier error de cualquiera de los errores anteriores, generalmente solo desea throwotros errores, para que su catchdeclaración tenga una lógica simple y consistente.

Por lo tanto, aunque puede poner un if-conditional en su catchy buscar errores falsos, en cuyo caso el caso de la verdad es trivial,

Either (Either Error ()) ()

probablemente preferirá la estructura lógica, al menos para lo que sale inmediatamente del autenticador, de un booleano más simple:

Either Error Bool

De hecho, el siguiente nivel de lógica de autenticación es probablemente devolver algún tipo de Userobjeto que contenga al usuario autenticado, de modo que esto se convierta en:

Either Error (Maybe User)

y esto es más o menos lo que esperaría: regresar nullen el caso de que el usuario no esté definido; de lo contrario, regresar {user_id: <number>, permission_to_launch_missiles: <boolean>}. Esperaría que el caso general de no iniciar sesión sea rescatable, por ejemplo, si estamos en algún tipo de modo "demostración a nuevos clientes", y no debería mezclarse con errores donde accidentalmente llamé object.doStuff()cuando object.doStuffestaba undefined.

Ahora con eso dicho, lo que es posible que desee hacer es definir una NotLoggedIno PermissionErrorexcepción que se deriva de Error. Luego, en las cosas que realmente lo necesitan, desea escribir:

function launchMissiles() {
    function actuallyLaunchThem() {
        // stub
    }
    return getAuth().then(auth => {
        if (auth === null) {
            throw new PermissionError('Cannot launch missiles without permission, cannot have permission if not logged in.');
        } else if (auth.permission_to_launch_missiles) {
            return actuallyLaunchThem();
        } else {
            throw new PermissionError(`User ${auth.user_id} does not have permission to launch the missiles.`);
        }
    });
}
CR Drost
fuente
3

Errores

Hablemos de errores.

Hay dos tipos de errores:

  • errores esperados
  • errores inesperados
  • errores fuera de uno

Errores esperados

Los errores esperados son estados en los que sucede algo incorrecto, pero usted sabe que podría ocurrir, por lo que debe lidiar con eso.

Estas son cosas como la entrada del usuario o solicitudes del servidor. Usted sabe que el usuario puede cometer un error o que el servidor puede estar inactivo, por lo que debe escribir un código de verificación para asegurarse de que el programa solicite información nuevamente, o muestre un mensaje, o cualquier otro comportamiento apropiado.

Estos son recuperables cuando se manejan. Si se deja sin control, se convierten en errores inesperados.

Errores inesperados

Los errores inesperados (errores) son estados en los que sucede algo incorrecto porque el código es incorrecto. Sabes que eventualmente sucederán, pero no hay forma de saber dónde o cómo lidiar con ellos porque, por definición, son inesperados.

Estas son cosas como la sintaxis y los errores lógicos. Puede tener un error tipográfico en su código, puede haber llamado a una función con los parámetros incorrectos. Estos no suelen ser recuperables.

try..catch

Hablemos de eso try..catch.

En JavaScript, throwno se usa comúnmente. Si busca ejemplos en el código, serán pocos y distantes entre sí, y generalmente estructurados de acuerdo con las líneas de

function example(param) {
  if (!Array.isArray(param) {
    throw new TypeError('"param" should be an array!');
  }
  ...
}

Debido a esto, los try..catchbloques tampoco son tan comunes para el flujo de control. Por lo general, es bastante fácil agregar algunas comprobaciones antes de llamar a los métodos para evitar los errores esperados.

Los entornos JavaScript también son bastante indulgentes, por lo que los errores inesperados a menudo también se dejan sin detectar.

try..catchno tiene que ser infrecuente Hay algunos casos de uso agradables, que son más comunes en lenguajes como Java y C #. Java y C # tienen la ventaja de las catchconstrucciones escritas , para que pueda diferenciar entre errores esperados e inesperados:

C # :
try
{
  var example = DoSomething();
}
catch (ExpectedException e)
{
  DoSomethingElse(e);
}

Este ejemplo permite que otras excepciones inesperadas fluyan y se manejen en otro lugar (como iniciar sesión y cerrar el programa).

En JavaScript, esta construcción se puede replicar a través de:

try {
  let example = doSomething();
} catch (e) {
  if (e instanceOf ExpectedError) {
    DoSomethingElse(e);
  } else {
    throw e;
  }
}

No es tan elegante, que es parte de la razón por la cual es poco común.

Las funciones

Hablemos de funciones.

Si utiliza el principio de responsabilidad única , cada clase y función debe cumplir un propósito singular.

Por ejemplo, authenticate()podría autenticar a un usuario.

Esto podría escribirse como:

const user = authenticate();
if (user == null) {
  // keep doing stuff
} else {
  // handle expected error
}

Alternativamente, podría escribirse como:

try {
  const user = authenticate();
  // keep doing stuff
} catch (e) {
  if (e instanceOf AuthenticationError) {
    // handle expected error
  } else {
    throw e;
  }
}

Ambos son aceptables.

Promesas

Hablemos de promesas.

Las promesas son una forma asincrónica de try..catch. Llamando new Promiseo Promise.resolvecomienza su trycódigo. Llamando throwo Promise.rejectte envía al catchcódigo.

Promise.resolve(value)   // try
  .then(doSomething)     // try
  .then(doSomethingElse) // try
  .catch(handleError)    // catch

Si tiene una función asincrónica para autenticar a un usuario, puede escribirla como:

authenticate()
  .then((user) => {
    if (user == null) {
      // keep doing stuff
    } else {
      // handle expected error
    }
  });

Alternativamente, podría escribirse como:

authenticate()
  .then((user) => {
    // keep doing stuff
  })
  .catch((e) => {
    if (e instanceOf AuthenticationError) {
      // handle expected error
    } else {
      throw e;
    }
  });

Ambos son aceptables.

Anidamiento

Hablemos de anidar.

try..catchpuede ser anidado Su authenticate()método puede tener internamente un try..catchbloque como:

try {
  const credentials = requestCredentialsFromUser();
  const user = getUserFromServer(credentials);
} catch (e) {
  if (e instanceOf CredentialsError) {
    // handle failure to request credentials
  } else if (e instanceOf ServerError) {
    // handle failure to get data from server
  } else {
    throw e; // no idea what happened
  }
}

Del mismo modo, las promesas se pueden anidar. Su authenticate()método asíncrono podría usar promesas internamente:

requestCredentialsFromUser()
  .then(getUserFromServer)
  .catch((e) => {
    if (e instanceOf CredentialsError) {
      // handle failure to request credentials
    } else if (e instanceOf ServerError) {
      // handle failure to get data from server
    } else {
      throw e; // no idea what happened
    }
  });

Entonces, ¿cuál es la respuesta?

Ok, creo que es hora de que realmente responda la pregunta:

¿Se considera que una falla en la autenticación rechazaría una promesa?

La respuesta más simple que puedo dar es que debe rechazar una promesa en cualquier lugar donde de lo contrario desearía throwuna excepción si fuera un código síncrono.

Si su flujo de control es más simple al hacer algunas ifverificaciones en sus thendeclaraciones, no hay necesidad de rechazar una promesa.

Si su flujo de control es más simple al rechazar una promesa y luego verificar los tipos de errores en su código de manejo de errores, hágalo en su lugar.

zzzzBov
fuente
0

He usado la rama "rechazar" de una Promesa para representar la acción "cancelar" de los cuadros de diálogo de jQuery UI. Parecía más natural que usar la rama "resolver", sobre todo porque a menudo hay múltiples opciones de "cerrar" en un cuadro de diálogo.

Alnitak
fuente
La mayoría de los puristas que conozco no estarían de acuerdo contigo.
0

Manejar una promesa es más o menos como una condición de "si". Depende de usted si desea "resolver" o "rechazar" si la autenticación falla.

evilReiko
fuente
1
La promesa es asincrónica try..catch, no if.
zzzzBov
@zzzBox, entonces, según esa lógica, debe usar una Promesa como asíncrona try...catchy simplemente decir que si pudo completar y obtener un resultado, debe resolver independientemente del valor recibido, de lo contrario, ¿debe rechazar?
@algo aquí, no, has malinterpretado mi argumento. try { if (!doSomething()) throw whatever; doSomethingElse() } catch { ... }está perfectamente bien, pero la construcción que Promiserepresenta a es la try..catchparte, no la ifparte.
zzzzBov
@zzzzBov Lo entendí con toda justicia :) Me gusta la analogía. Pero mi lógica es simplemente que si doSomething()falla, arrojará, pero si no podría contener el valor que necesita (lo ifanterior es un poco confuso ya que no es parte de su idea aquí :)). Solo debe rechazar si hay una razón para lanzar (en la analogía), por lo tanto, si la prueba falló. Si la prueba tuvo éxito, siempre debe resolver, independientemente de si su valor es positivo, ¿verdad?
@ en algún lugar, he decidido escribir una respuesta (suponiendo que esto permanezca abierto el tiempo suficiente), porque los comentarios no son suficientes para expresar mis pensamientos.
zzzzBov