Si sta rifiutando una promessa solo per i casi di errore?

21

Diciamo che ho questa funzione autenticare che restituisce una promessa. La promessa quindi si risolve con il risultato. I risultati falsi e veri sono attesi, come vedo io, e i rifiuti dovrebbero verificarsi solo in un caso di errore. Oppure, un errore nell'autenticazione è considerato qualcosa per cui rifiuteresti una promessa?

    
posta Mathieu Bertin 02.12.2016 - 16:09
fonte

5 risposte

17

Buona domanda! Non c'è una risposta difficile. Dipende da ciò che consideri eccezionale in quel punto specifico del flusso .

Rifiutare un Promise equivale a sollevare un'eccezione. Non tutti i risultati indesiderati sono eccezionali , il risultato di errori . Potresti argomentare il tuo caso in entrambi i modi:

  1. L'autenticazione fallita dovrebbe reject il Promise , perché il chiamante si aspetta un oggetto User in cambio, e qualsiasi altra cosa è un'eccezione per questo flusso.

  2. L'autenticazione fallita dovrebbe resolve la Promise , anche se null , poiché fornire le credenziali sbagliate non è in realtà un caso eccezionale e il chiamante non dovrebbe aspettarsi il flusso per generare sempre un User .

Nota che sto esaminando il problema dal lato del chiamante . Nel flusso di informazioni, il chiamante si aspetta le sue azioni per generare un User (e qualsiasi altra cosa è un errore), o ha senso che questo particolare chiamante gestisca altri risultati?

In un sistema a più livelli, la risposta potrebbe cambiare quando i dati fluiscono attraverso i livelli. Ad esempio:

  • Il livello HTTP dice RISOLVI! La richiesta è stata inviata, il socket è stato chiuso in modo pulito e il server ha emesso una risposta valida. L' API di recupero fa questo.
  • Protocollo layer quindi dice REJECT! Il codice di stato nella risposta era 401, che è ok per HTTP, ma non per il protocollo!
  • Il livello di autenticazione dice NO, RISOLVA! Rileva l'errore, poiché 401 è lo stato previsto per una password errata e si risolve in un null utente.
  • Il controller dell'interfaccia dice NESSUNO DI QUESTO, RIFIUTATO! La visualizzazione modale sullo schermo si aspettava un nome utente e un avatar e, a questo punto, qualsiasi altra informazione è un errore.

Questo esempio in 4 punti è ovviamente complicato, ma illustra 2 punti:

  1. Se una cosa è un'eccezione / rifiuto o meno dipende dal flusso circostante e dalle aspettative
  2. Diversi livelli del tuo programma possono trattare lo stesso risultato in modo diverso, poiché si trovano in varie fasi del flusso

Quindi ancora una volta, nessuna risposta difficile. È tempo di pensare e progettare!

    
risposta data 02.12.2016 - 16:27
fonte
6

Quindi Promises ha una buona proprietà che portano JS dai linguaggi funzionali, che è che implementano effettivamente questo costruttore di tipi Either che unisce due altri tipi, il tipo Left e il tipo Right , da forzare la logica a prendere l'uno o l'altro ramo.

data Either x y = Left x | Right y

Ora stai notando che il tipo sul lato sinistro è ambiguo per le promesse; puoi rifiutare con qualsiasi cosa Questo è vero perché JS è debolmente digitato, ma tu vuoi essere cauto se stai programmando in modo difensivo.

Il motivo è che JS prende throw istruzioni dal codice di gestione delle promesse e lo impacchetta nel lato Left di quello. Tecnicamente in JS puoi throw di tutto, incluso true / false o una stringa o un numero: ma il codice JavaScript lancia anche cose senza throw (quando fai cose come provare ad accedere a proprietà su null ) e c'è un'API risolta per questo (l'oggetto Error ). Quindi, quando si va in giro a prendere, di solito è bello poter supporre che quegli errori siano oggetti Error . E poiché reject per la promessa si agglomera in tutti gli errori di uno dei suddetti bug, generalmente vuoi solo throw altri errori, per rendere la tua dichiarazione catch una logica semplice e coerente.

Pertanto anche se puoi inserire un condizionale condizionale nel tuo catch e cercare errori falsi, nel qual caso il caso di verità è banale,

Either (Either Error ()) ()

probabilmente preferirai la struttura logica, almeno per ciò che viene immediatamente dall'autenticatore, di un semplice booleano:

Either Error Bool

In effetti, il prossimo livello di logica di autenticazione è probabilmente quello di restituire una specie di oggetto User contenente l'utente autenticato, in modo che questo diventi:

Either Error (Maybe User)

e questo è più o meno quello che mi aspetterei: return null nel caso in cui l'utente non sia definito, altrimenti restituisce {user_id: <number>, permission_to_launch_missiles: <boolean>} . Mi aspetto che il caso generale di non essere loggato sia recuperabile, ad esempio se siamo in una sorta di modalità "demo per nuovi clienti", e non dovremmo essere confusi con bug in cui ho accidentalmente chiamato object.doStuff() quando object.doStuff era undefined .

Detto questo, ciò che si potrebbe voler fare è definire un'eccezione NotLoggedIn o PermissionError che deriva da Error . Poi nelle cose che ne hanno davvero bisogno, vuoi scrivere:

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.');
        }
    });
}
    
risposta data 02.12.2016 - 16:28
fonte
3

Errori

Parliamo degli errori.

Esistono due tipi di errori:

  • errori previsti
  • errori imprevisti
  • errori off-by-one

Errori previsti

Gli errori previsti sono stati in cui accade la cosa sbagliata ma sai che potrebbe, quindi ti occupi di esso.

Sono cose come input dell'utente o richieste del server. Sai che l'utente potrebbe commettere un errore o che il server potrebbe essere inattivo, quindi scrivi un codice di controllo per assicurarti che il programma chieda di nuovo l'input, o che visualizzi un messaggio o qualunque altro comportamento sia appropriato.

Questi sono recuperabili quando gestiti. Se lasciati a mano, diventano errori imprevisti.

Errori imprevisti

Errori imprevisti (bug) sono stati in cui accade la cosa sbagliata perché il codice è sbagliato. Sai che alla fine succederanno, ma non c'è modo di sapere dove o come affrontarli perché, per definizione, sono inaspettati.

Sono cose come sintassi e errori logici. Potresti avere un errore di digitazione nel tuo codice, potresti avere chiamato una funzione con i parametri sbagliati. Questi non sono in genere recuperabili.

try..catch

Parliamo di try..catch .

In JavaScript, throw non è comunemente usato. Se ti guardi intorno con degli esempi di codice, saranno pochi e distanti tra loro e di solito sono strutturati sulla falsariga di

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

Per questo motivo, anche i blocchi try..catch non sono tutti così comuni per il flusso di controllo. Di solito è abbastanza semplice aggiungere alcuni controlli prima di chiamare i metodi per evitare errori attesi.

Anche gli ambienti JavaScript sono abbastanza indulgenti, quindi anche gli errori imprevisti vengono spesso ignorati.

try..catch non deve essere raro. Ci sono alcuni casi d'uso, che sono più comuni in linguaggi come Java e C #. Java e C # hanno il vantaggio di aver digitato catch costrutti, in modo da poter distinguere tra errori attesi e imprevisti:

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

Questo esempio consente ad altre eccezioni impreviste di scorrere e di essere gestite altrove (ad esempio registrandosi e chiudendo il programma).

In JavaScript, questo costrutto può essere replicato tramite:

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

Non elegante, che è parte del motivo per cui è raro.

Funzioni

Parliamo delle funzioni.

Se utilizzi il principio di responsabilità singola , ogni classe e funzione dovrebbe avere uno scopo singolare.

Ad esempio, authenticate() potrebbe autenticare un utente.

Potrebbe essere scritto come:

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

In alternativa potrebbe essere scritto come:

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

Entrambi sono accettabili.

La promessa

Parliamo di promesse.

Le promesse sono una forma asincrona di try..catch . Chiamando new Promise o Promise.resolve inizia il tuo codice try . Chiamando throw o Promise.reject ti viene inviato il codice catch .

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

Se hai una funzione asincrona per autenticare un utente, puoi scriverlo come:

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

In alternativa potrebbe essere scritto come:

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

Entrambi sono accettabili.

Nesting

Parliamo di nidificazione.

try..catch può essere annidato. Il tuo metodo authenticate() potrebbe avere internamente un blocco try..catch come:

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
  }
}

Allo stesso modo le promesse possono essere annidate. Il tuo metodo async authenticate() potrebbe utilizzare internamente promesse:

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
    }
  });

Quindi qual è la risposta?

Ok, penso che sia giunto il momento per me di rispondere effettivamente alla domanda:

Is a failure in authentication considered something you would reject a promise for?

La risposta più semplice che posso dare è che dovresti rifiutare una promessa ovunque vorrai altrimenti throw un'eccezione se fosse un codice sincrono.

Se il tuo flusso di controllo è più semplice con alcuni controlli if nelle tue dichiarazioni then , non è necessario rifiutare una promessa.

Se il tuo flusso di controllo è più semplice rifiutando una promessa e controllando i tipi di errori nel codice di gestione degli errori, fallo invece.

    
risposta data 02.12.2016 - 17:24
fonte
0

Gestire una promessa è più o meno come la condizione "se". Spetta a te decidere se "risolvere" o "rifiutare" se l'autenticazione fallisce.

    
risposta data 02.12.2016 - 16:16
fonte
0

Ho usato il ramo "reject" di una Promise per rappresentare l'azione "cancel" delle finestre di dialogo dell'interfaccia utente jQuery. Sembrava più naturale dell'usare il ramo "risoluzione", non ultimo perché spesso ci sono più opzioni "chiuse" in una finestra di dialogo.

    
risposta data 02.12.2016 - 16:30
fonte

Leggi altre domande sui tag