Rifiutare una promessa è solo per casi di errore?


25

Diciamo che ho questa funzione di autenticazione che restituisce una promessa. La promessa si risolve quindi con il risultato. I risultati attesi sono falsi e veri, a mio avviso, e il rifiuto dovrebbe verificarsi solo in un caso di errore. Oppure, un errore nell'autenticazione è considerato qualcosa per cui rifiuteresti una promessa?


Se l'autenticazione fallisce, dovresti rejecte non restituire false, ma se ti aspetti che il valore sia a Bool, allora hai avuto successo e dovresti risolvere con il Bool indipendentemente dal valore. Le promesse sono una specie di proxy per i valori: memorizzano il valore restituito, quindi solo se il valore non può essere ottenuto dovresti reject. Altrimenti dovresti resolve.

Questa è una buona domanda Tocca uno dei fallimenti del progetto promettente. Esistono due tipi di errori, errori previsti, ad esempio quando un utente fornisce un input errato (come l'accesso non riuscito) e errori imprevisti, che sono bug nel codice. Il design promettente unisce i due concetti in un unico flusso rendendo difficile distinguerli per la gestione.
zzzzBov,

1
Direi che risolvere significa utilizzare la risposta e continuare l'applicazione, mentre rifiutare significa annullare l'operazione corrente (e possibilmente riprovare o fare qualcos'altro).

4
un altro modo di pensarci: se si trattasse di una chiamata al metodo sincrono, considereresti il ​​normale fallimento dell'autenticazione (nome utente / password errati) come una restituzione falseo come un'eccezione?
wrschneider,

2
L' API Fetch ne è un buon esempio. Si attiva sempre thenquando il server risponde, anche se viene restituito un codice di errore, ed è necessario controllare il response.ok. Il catchgestore viene attivato solo per errori imprevisti .
CodingIntrigue

Risposte:


22

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

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

  1. Autenticazione fallita dovrebbero rejectla Promise, perché il chiamante si aspetta un Useroggetto in cambio, e tutto il resto è un'eccezione a questo flusso.

  2. Autenticazione fallita dovrebbero resolvela Promise, seppur null, in quanto fornire le credenziali errate non è davvero un eccezionale caso, e il chiamante non deve aspettarsi il flusso per sempre come risultato un User.

Si noti che sto esaminando il problema dal lato del chiamante . Nel flusso di informazioni, il chiamante si aspetta che le sue azioni provochino un User(e qualcos'altro è un errore) o ha senso che questo particolare chiamante gestisca altri risultati?

In un sistema a più livelli, la risposta può cambiare mentre i dati scorrono attraverso i livelli. Per esempio:

  • Il livello HTTP dice RESOLVE! La richiesta è stata inviata, il socket è stato chiuso in modo pulito e il server ha emesso una risposta valida. L' API Fetch esegue questa operazione.
  • Il protocollo 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, RISOLVE! Cattura l'errore, poiché 401 è lo stato previsto per una password errata e si risolve in un nullutente.
  • Il controller di interfaccia dice NESSUNO DI QUELLO, RIFIUTA! La visualizzazione modale sullo schermo si aspettava un nome utente e un avatar, e qualsiasi cosa diversa da quella informazione è un errore a questo punto.

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

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

Quindi di nuovo, nessuna risposta difficile. È tempo di pensare e progettare!


6

Quindi le promesse hanno una bella proprietà che portano JS dai linguaggi funzionali, ovvero implementano effettivamente questo Eithercostruttore di tipi che unisce due altri tipi, il Lefttipo e il Righttipo, forzando la logica a prendere un ramo o l'altro ramo.

data Either x y = Left x | Right y

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

Il motivo è che JS prenderà le throwdichiarazioni dal codice di gestione delle promesse e lo aggregherà anche a Leftquesto. Tecnicamente in JS puoi fare throwtutto, incluso vero / falso o una stringa o un numero: ma il codice JavaScript lancia anche le cose senzathrow (quando fai cose come provare ad accedere alle proprietà sui null) e c'è un'API stabilita per questo (l' Erroroggetto) . Quindi, quando si arriva alla cattura, di solito è bello poter supporre che quegli errori siano Erroroggetti. E dal momento rejectche la promessa si aggraverà in eventuali errori di uno qualsiasi dei bug di cui sopra, in genere si desidera solo throwaltri errori, per rendere la propria catchaffermazione avere una logica semplice e coerente.

Pertanto, anche se puoi mettere un if-condizionale nel tuo catche cercare falsi errori, nel qual caso il caso della verità è banale,

Either (Either Error ()) ()

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

Either Error Bool

In effetti il ​​livello successivo della logica di autenticazione è probabilmente quello di restituire una sorta di Useroggetto contenente l'utente autenticato, in modo che questo diventi:

Either Error (Maybe User)

e questo è più o meno quello che mi aspetterei: ritorna nullnel caso in cui l'utente non sia definito, altrimenti ritorna {user_id: <number>, permission_to_launch_missiles: <boolean>}. Mi aspetto che il caso generale di non essere loggato sia salvabile, ad esempio se ci troviamo in una sorta di modalità "demo per nuovi clienti", e non dovremmo mescolarli con bug dove accidentalmente ho chiamato object.doStuff()quando lo object.doStuffero undefined.

Ora, detto ciò, ciò che potresti voler fare è definire una NotLoggedIno PermissionErrorun'eccezione da cui deriva Error. Quindi 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.`);
        }
    });
}

3

Errori

Parliamo di 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 accadere, quindi devi affrontarla.

Queste sono cose come l'input dell'utente o le 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 richieda di nuovo l'input o visualizzi un messaggio o qualunque altro comportamento sia appropriato.

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

Errori imprevisti

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

Queste sono cose come la sintassi e gli errori logici. Potresti avere un refuso nel tuo codice, potresti aver chiamato una funzione con parametri errati. Questi non sono in genere recuperabili.

try..catch

Parliamo di try..catch.

In JavaScript, thrownon è comunemente usato. Se cerchi esempi nel codice, saranno pochi e distanti tra loro e di solito strutturati lungo le linee di

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

Per questo try..catchmotivo , neanche i blocchi sono così comuni per il flusso di controllo. Di solito è abbastanza facile aggiungere alcuni controlli prima di chiamare i metodi per evitare errori previsti.

Anche gli ambienti JavaScript sono abbastanza indulgenti, quindi anche gli errori imprevisti vengono lasciati non rilevati.

try..catchnon deve essere insolito. Ci sono alcuni casi d'uso simpatici, che sono più comuni in linguaggi come Java e C #. Java e C # hanno il vantaggio di catchcostrutti tipizzati , in modo da poter distinguere tra errori previsti e imprevisti:

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

Questo esempio consente ad altre eccezioni impreviste di fluire 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 così elegante, il che è parte del motivo per cui è raro.

funzioni

Parliamo di funzioni.

Se usi il principio della responsabilità singola , ogni classe e funzione dovrebbe servire a uno scopo singolare.

Ad esempio authenticate()potrebbe autenticare un utente.

Questo 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.

promesse

Parliamo di promesse.

Le promesse sono una forma asincrona di try..catch. Chiama new Promiseo Promise.resolveavvia il tuo trycodice. Chiamata throwo Promise.rejectinvio al catchcodice.

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

Se si dispone di una funzione asincrona per autenticare un utente, è possibile 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.

annidamento

Parliamo di nidificazione.

try..catchpuò essere nidificato. Il tuo authenticate()metodo potrebbe avere internamente un try..catchblocco 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 nidificate. Il tuo authenticate()metodo asincrono potrebbe utilizzare internamente le 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 tempo per me di rispondere effettivamente alla domanda:

Un errore nell'autenticazione è considerato qualcosa per cui rifiuteresti una promessa?

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

Se il flusso di controllo è più semplice con alcuni ifcontrolli nelle thendichiarazioni, non è necessario rifiutare una promessa.

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


0

Ho usato il ramo "rifiuta" di una promessa per rappresentare l'azione "annulla" delle finestre di dialogo dell'interfaccia utente di jQuery. Sembrava più naturale che usare il ramo "risoluzione", anche perché spesso ci sono più opzioni "chiudi" in una finestra di dialogo.


Molti puristi che conosco non sarebbero d'accordo con te.

0

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


1
la promessa è un asincrono try..catch, no if.
zzzzBov,

@zzzBox quindi con quella logica, dovresti usare una Promessa come asincrona try...catche dire semplicemente che se tu fossi in grado di completare e ottenere un risultato, dovresti risolvere indipendentemente dal valore che è stato ricevuto, altrimenti dovresti rifiutare?

@somethinghere, no, hai frainteso il mio argomento. try { if (!doSomething()) throw whatever; doSomethingElse() } catch { ... }va benissimo, ma il costrutto che a Promiserappresenta è la try..catchparte, non la ifparte.
zzzzBov,

@zzzzBov L'ho capito in tutta onestà :) Mi piace l'analogia. Ma la mia logica è semplicemente che se doSomething()fallisce, allora lancerà, ma in caso contrario potrebbe contenere il valore di cui hai bisogno (il tuo ifsopra è leggermente confuso perché non fa parte della tua idea qui :)). Dovresti rifiutare solo se c'è un motivo per lanciare (nell'analogia), quindi se il test fallisce. Se il test ha esito positivo, dovresti sempre risolvere, indipendentemente dal fatto che il suo valore sia positivo, giusto?

@somethinghere, ho deciso di scrivere una risposta (supponendo che questo rimanga aperto abbastanza a lungo), perché i commenti non sono sufficienti per esprimere i miei pensieri.
zzzzBov,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.