Devo tornare dopo una risoluzione / rifiuto precoce?


262

Supponiamo che io abbia il seguente codice.

function divide(numerator, denominator) {
 return new Promise((resolve, reject) => {

  if(denominator === 0){
   reject("Cannot divide by 0");
   return; //superfluous?
  }

  resolve(numerator / denominator);

 });
}

Se il mio obiettivo è quello di utilizzare rejectper uscire presto, dovrei prendere l'abitudine di returning anche dopo?


5
Sì, a causa del completamento

Risposte:


371

Lo returnscopo è di terminare l'esecuzione della funzione dopo il rifiuto e impedire l'esecuzione del codice dopo di essa.

function divide(numerator, denominator) {
  return new Promise((resolve, reject) => {

    if (denominator === 0) {
      reject("Cannot divide by 0");
      return; // The function execution ends here 
    }

    resolve(numerator / denominator);
  });
}

In questo caso impedisce l' resolve(numerator / denominator);esecuzione, il che non è strettamente necessario. Tuttavia, è ancora preferibile terminare l'esecuzione per prevenire una possibile trappola in futuro. Inoltre, è una buona pratica impedire inutilmente l'esecuzione del codice.

sfondo

Una promessa può essere in uno dei 3 stati:

  1. in sospeso - stato iniziale. Da in attesa possiamo spostarci in uno degli altri stati
  2. adempiuto - operazione riuscita
  3. rifiutato - operazione fallita

Quando una promessa viene adempiuta o respinta, rimarrà in questo stato indefinitamente (risolto). Pertanto, il rifiuto di una promessa mantenuta o l'adempimento di una promessa respinta non avranno effetto.

Questo frammento di esempio mostra che sebbene la promessa sia stata mantenuta dopo essere stata respinta, è rimasta respinta.

function divide(numerator, denominator) {
  return new Promise((resolve, reject) => {
    if (denominator === 0) {
      reject("Cannot divide by 0");
    }

    resolve(numerator / denominator);
  });
}

divide(5,0)
  .then((result) => console.log('result: ', result))
  .catch((error) => console.log('error: ', error));

Quindi perché dobbiamo tornare?

Sebbene non possiamo modificare uno stato promesso stabile, il rifiuto o la risoluzione non interromperà l'esecuzione del resto della funzione. La funzione può contenere codice che creerà risultati confusi. Per esempio:

function divide(numerator, denominator) {
  return new Promise((resolve, reject) => {
    if (denominator === 0) {
      reject("Cannot divide by 0");
    }
    
    console.log('operation succeeded');

    resolve(numerator / denominator);
  });
}

divide(5, 0)
  .then((result) => console.log('result: ', result))
  .catch((error) => console.log('error: ', error));

Anche se la funzione non contiene tale codice in questo momento, questo crea una possibile trappola futura. Un futuro refactor potrebbe ignorare il fatto che il codice viene ancora eseguito dopo che la promessa è stata respinta e sarà difficile eseguire il debug.

Interruzione dell'esecuzione dopo la risoluzione / rifiuto:

Questo è materiale standard per il flusso di controllo JS.

  • Ritorna dopo il resolve/ reject:

  • Restituisci con resolve/ reject- poiché il valore restituito del callback viene ignorato, possiamo salvare una riga restituendo la dichiarazione di rifiuto / risoluzione:

  • Utilizzando un blocco if / else:

Preferisco usare una delle returnopzioni in quanto il codice è più piatto.


28
Vale la pena notare che il codice non si comporterà effettivamente diversamente se returnè presente o meno perché una volta impostato uno stato promettente, non può essere modificato, quindi chiamare resolve()dopo aver chiamato reject()non farà nulla se non l'uso di alcuni cicli CPU aggiuntivi. Io stesso utilizzerei il returngiusto dal punto di vista della pulizia e dell'efficienza del codice, ma non è richiesto in questo esempio specifico.
jfriend00,

1
Prova a utilizzare Promise.try(() => { })invece di New Promise ed evita di utilizzare le chiamate di risoluzione / rifiuto. Invece potresti semplicemente scrivere return denominator === 0 ? throw 'Cannot divide by zero' : numerator / denominator; che uso Promise.trycome mezzo per dare il via a una Promessa e per eliminare le promesse racchiuse in blocchi try / catch che sono problematici.
Kingdango,

2
È buono a sapersi, e mi piace lo schema. Tuttavia, in questo momento Promise.try è un suggerimento fase 0, quindi puoi usarlo solo con uno spessore o usando una libreria di promesse come bluebird o Q.
Ori Drori,

6
@ jfriend00 Ovviamente in questo semplice esempio il codice non si comporterà diversamente. E se avessi il codice dopo quello rejectche fa qualcosa di costoso, come connettersi a database o endpoint API? Sarebbe tutto inutile e ti costerebbe denaro e risorse, soprattutto per esempio se ti stai collegando a qualcosa come un database AWS o un endpoint gateway API. In tal caso, utilizzeresti sicuramente un ritorno per evitare l'esecuzione di codice non necessario.
Jake Wilson,

3
@JakeWilson - Certo, questo è normale flusso di codice in Javascript e non ha nulla a che fare con le promesse. Se hai terminato di elaborare la funzione e non desideri più eseguire codice nel percorso del codice corrente, inserisci a return.
jfriend00,

37

Un linguaggio comune, che può essere o meno la tua tazza di tè, è quello di combinare il returncon il reject, per rifiutare simultaneamente la promessa e uscire dalla funzione, in modo che il resto della funzione incluso il resolvenon sia eseguito. Se ti piace questo stile, può rendere il tuo codice un po 'più compatto.

function divide(numerator, denominator) {
  return new Promise((resolve, reject) => {
    if (denominator === 0) return reject("Cannot divide by 0");
                           ^^^^^^^^^^^^^^
    resolve(numerator / denominator);
  });
}

Questo funziona bene perché il costruttore Promessa non fa nulla con qualsiasi valore di ritorno, e in ogni caso resolvee rejectritorno nulla.

Lo stesso linguaggio può essere utilizzato con lo stile di callback mostrato in un'altra risposta:

function divide(nom, denom, cb){
  if(denom === 0) return cb(Error("Cannot divide by zero"));
                  ^^^^^^^^^
  cb(null, nom / denom);
} 

Ancora una volta, questo funziona bene perché la persona che chiama dividenon si aspetta che restituisca qualcosa e non fa nulla con il valore restituito.


6
Non mi piace questo. Questo dà l'idea che stai restituendo qualcosa che in realtà non sei. Si sta invocando la funzione di rifiuto e quindi si utilizza return per terminare l'esecuzione della funzione. Tienili su linee separate, ciò che stai facendo confonderà solo le persone. La leggibilità del codice è re.
K - La tossicità in SO sta crescendo.

7
@KarlMorrison stai infatti restituendo "qualcosa", una promessa respinta. Penso che la "nozione" di cui stai parlando sia molto personale. Non c'è niente di sbagliato nel restituire uno rejectstatus
Frondor

5
@Frondor Non penso che tu abbia capito cosa ho scritto. Ovviamente io e te lo capiamo, non succede nulla quando si restituisce un rifiuto sulla stessa linea. Ma che dire degli sviluppatori che non sono così abituati a JavaScript entrare in un progetto? Questo tipo di programmazione riduce la leggibilità per tali persone. L'ecosistema JavaScript oggi è abbastanza complicato e le persone che diffondono questo tipo di pratiche non faranno altro che peggiorare. Questa è una cattiva pratica.
K - La tossicità in SO sta crescendo.

1
@KarlMorrison Opinioni personali! = Cattiva pratica. Probabilmente aiuterebbe un nuovo sviluppatore di Javascript a capire cosa sta succedendo con il ritorno.
Toby Caulk,

1
@TobyCaulk Se le persone hanno bisogno di sapere che cosa fa il ritorno, allora non dovrebbero giocare con Promises, dovrebbero imparare la programmazione di base.
K - La tossicità in SO sta crescendo.

10

Tecnicamente non è necessario qui 1 - perché una Promessa può essere risolta o respinta, esclusivamente e solo una volta. Il primo risultato Promessa vince e ogni risultato successivo viene ignorato. Ciò è diverso dai callback in stile nodo.

Detto questo, è una buona pratica pulita garantire che venga chiamato esattamente uno, quando pratico, e in questo caso in quanto non vi è ulteriore elaborazione asincrona / differita. La decisione di "tornare presto" non è diversa dalla cessazione di qualsiasi funzione quando il suo lavoro è completo, piuttosto che continuare l'elaborazione non correlata o non necessaria.

Restituire al momento opportuno (o altrimenti usare condizionali per evitare di eseguire il caso "altro") riduce la possibilità di eseguire accidentalmente codice in uno stato non valido o eseguire effetti collaterali indesiderati; e come tale rende il codice meno incline a "rompersi inaspettatamente".


1 Questa risposta tecnica dipende anche dal fatto che in questo caso il codice dopo il "return", qualora fosse omesso, non si tradurrà in un effetto collaterale. JavaScript si dividerà felicemente per zero e restituirà + Infinito / -Infinito o NaN.


Bella nota !!
HankCa

9

Se non "ritorni" dopo una risoluzione / rifiuto, possono succedere cose cattive (come un reindirizzamento della pagina) dopo che volevi fermarlo. Fonte: mi sono imbattuto in questo.


6
+1 per l'esempio. Ho avuto un problema in cui il mio programma avrebbe fatto più di 100 query di database non valide e non riuscivo a capire perché. Si scopre che non ho "restituito" dopo un rifiuto. È un piccolo errore ma ho imparato la mia lezione.
AdamInTheOculus

8

La risposta di Ori spiega già che non è necessario returnma è una buona pratica. Nota che il costruttore della promessa è sicuro, quindi ignorerà le eccezioni generate più avanti nel percorso, essenzialmente hai effetti collaterali che non puoi facilmente osservare.

Si noti che returning presto è anche molto comune nei callback:

function divide(nom, denom, cb){
     if(denom === 0){
         cb(Error("Cannot divide by zero");
         return; // unlike with promises, missing the return here is a mistake
     }
     cb(null, nom / denom); // this will divide by zero. Since it's a callback.
} 

Quindi, sebbene sia una buona pratica nelle promesse, è necessario con callback. Alcune note sul tuo codice:

  • Il tuo caso d'uso è ipotetico, in realtà non usare promesse con azioni sincrone.
  • Il costruttore della promessa ignora i valori di ritorno. Alcune biblioteche avviseranno se si restituisce un valore non indefinito per avvisarti dell'errore di tornare lì. La maggior parte non è così intelligente.
  • Il costruttore della promessa è sicuro, convertirà le eccezioni in rifiuti ma, come altri hanno sottolineato, una promessa si risolve una volta.

4

In molti casi è possibile convalidare i parametri separatamente e restituire immediatamente una promessa rifiutata con Promise.reject (motivo) .

function divide2(numerator, denominator) {
  if (denominator === 0) {
    return Promise.reject("Cannot divide by 0");
  }
  
  return new Promise((resolve, reject) => {
    resolve(numerator / denominator);
  });
}


divide2(4, 0).then((result) => console.log(result), (error) => console.log(error));

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.