Promesse di JavaScript: rifiuto contro lancio


385

Ho letto diversi articoli su questo argomento, ma non mi è ancora chiaro se c'è una differenza tra Promise.rejectvs. lanciare un errore. Per esempio,

Utilizzando Promise.reject

return asyncIsPermitted()
    .then(function(result) {
        if (result === true) {
            return true;
        }
        else {
            return Promise.reject(new PermissionDenied());
        }
    });

Usando il tiro

return asyncIsPermitted()
    .then(function(result) {
        if (result === true) {
            return true;
        }
        else {
            throw new PermissionDenied();
        }
    });

La mia preferenza è quella di usare throwsemplicemente perché è più corto, ma mi chiedevo se ci fosse qualche vantaggio rispetto all'altro.


9
Entrambi i metodi producono esattamente la stessa risposta. Il .then()gestore rileva l'eccezione generata e la trasforma automaticamente in una promessa rifiutata. Dato che ho letto che le eccezioni generate non sono particolarmente veloci da eseguire, immagino che restituire la promessa respinta potrebbe essere leggermente più veloce da eseguire, ma dovresti escogitare un test su più browser moderni se fosse importante saperlo. Personalmente uso throwperché mi piace la leggibilità.
jfriend00,

@webduvet non con Promises: sono progettati per funzionare con il lancio.
joews,

15
Uno svantaggio throwè che non si tradurrebbe in una promessa rifiutata se fosse stata lanciata da un callback asincrono, come setTimeout. jsfiddle.net/m07van33 @Blondie la tua risposta è stata corretta.
Kevin B,

@joews non significa che sia buono;)
webduvet

1
Ah vero. Quindi un chiarimento al mio commento sarebbe "se fosse stato lanciato da un callback asincrono che non era stato promesso " . Sapevo che c'era un'eccezione, non riuscivo a ricordare di cosa si trattasse. Anch'io preferisco usare il lancio semplicemente perché lo trovo più leggibile e mi consente di ometterlo rejectdal mio elenco di parametri.
Kevin B,

Risposte:


346

Non vi è alcun vantaggio nell'usare l'uno rispetto all'altro, ma esiste un caso specifico in cui thrownon funzionerà. Tuttavia, questi casi possono essere risolti.

Ogni volta che ti trovi in ​​un callback promettente, puoi usarlo throw. Tuttavia, se si è in qualsiasi altro callback asincrono, è necessario utilizzare reject.

Ad esempio, questo non attiverà il problema:

new Promise(function() {
  setTimeout(function() {
    throw 'or nah';
    // return Promise.reject('or nah'); also won't work
  }, 1000);
}).catch(function(e) {
  console.log(e); // doesn't happen
});

Invece ti rimane una promessa irrisolta e un'eccezione non rilevata. Questo è un caso in cui vorresti invece usare reject. Tuttavia, potresti risolverlo in due modi.

  1. usando la funzione di rifiuto originale di Promise all'interno del timeout:

new Promise(function(resolve, reject) {
  setTimeout(function() {
    reject('or nah');
  }, 1000);
}).catch(function(e) {
  console.log(e); // works!
});

  1. promettendo il timeout:

function timeout(duration) { // Thanks joews
  return new Promise(function(resolve) {
    setTimeout(resolve, duration);
  });
}

timeout(1000).then(function() {
  throw 'worky!';
  // return Promise.reject('worky'); also works
}).catch(function(e) {
  console.log(e); // 'worky!'
});


54
Vale la pena ricordare che i luoghi all'interno di un callback asincrono non promesso che non è possibile utilizzare throw error, non è inoltre possibile utilizzare return Promise.reject(err)quale è ciò che l'OP ci chiedeva di confrontare. Questo è fondamentalmente il motivo per cui non si dovrebbero inserire richiami asincroni all'interno delle promesse. Promuovi tutto ciò che è asincrono e quindi non hai queste restrizioni.
jfriend00,

9
"Tuttavia, se sei in qualsiasi altro tipo di callback" dovrebbe essere "Tuttavia, se sei in qualsiasi altro tipo di callback asincrono ". I callback possono essere sincroni (ad es. Con Array#forEach) e con quelli, il lancio al loro interno funzionerebbe.
Félix Saparelli,

2
@KevinB leggendo queste righe "c'è un caso specifico in cui il tiro non funziona." e "Ogni volta che ti trovi all'interno di un callback promettente, puoi usare il lancio. Tuttavia, se sei in qualsiasi altro callback asincrono, devi usare il rifiuto." Ho la sensazione che gli snippet di esempio mostreranno casi in cui thrownon funzionerà e invece Promise.rejectè una scelta migliore. Tuttavia, i frammenti non sono interessati da nessuna di queste due scelte e danno lo stesso risultato indipendentemente da ciò che si sceglie. Mi sto perdendo qualcosa?
Anshul,

2
sì. se usi il lancio in un setTimeout, la cattura non verrà chiamata. è necessario utilizzare rejectciò che è stato passato al new Promise(fn)callback.
Kevin B,

2
@KevinB grazie per essere stati d'accordo. L'esempio fornito dall'OP menziona specificamente il confronto return Promise.reject()e throw. Non menziona il rejectcallback dato nel new Promise(function(resolve, reject))costrutto. Quindi, mentre i due frammenti dimostrano giustamente quando dovresti usare il callback di risoluzione, la domanda di OP non era quella.
Anshul,

202

Un altro fatto importante è che reject() NON termina il flusso di controllo come returnfa un'istruzione. Al contrario throw, termina il flusso di controllo.

Esempio:

new Promise((resolve, reject) => {
  throw "err";
  console.log("NEVER REACHED");
})
.then(() => console.log("RESOLVED"))
.catch(() => console.log("REJECTED"));

vs

new Promise((resolve, reject) => {
  reject(); // resolve() behaves similarly
  console.log("ALWAYS REACHED"); // "REJECTED" will print AFTER this
})
.then(() => console.log("RESOLVED"))
.catch(() => console.log("REJECTED"));


51
Bene, il punto è corretto ma il confronto è complicato. Perché normalmente dovresti restituire la promessa respinta scrivendo return reject(), quindi la riga successiva non verrà eseguita.
AZ.

7
Perché vorresti restituirlo?
Lukyer,

31
In questo caso, return reject()è semplicemente una scorciatoia per cui reject(); returnciò che si desidera è interrompere il flusso. Il valore restituito dall'esecutore (la funzione passata a new Promise) non viene utilizzato, quindi è sicuro.
Félix Saparelli,

47

Sì, la differenza più grande è quella rifiuto è una funzione di richiamata che viene eseguita dopo il rifiuto della promessa, mentre il lancio non può essere utilizzato in modo asincrono. Se si sceglie di utilizzare il rifiuto, il codice continuerà a funzionare normalmente in modo asincrono, mentre lancio darà la priorità al completamento della funzione di risoluzione (questa funzione verrà eseguita immediatamente).

Un esempio che ho visto che mi ha aiutato a chiarire il problema per me è stato che potresti impostare una funzione di timeout con rifiuto, ad esempio:

new Promise(_, reject) {
 setTimeout(reject, 3000);
});

Quanto sopra non sarebbe possibile scrivere con il lancio.

Nel tuo piccolo esempio la differenza in indistinguibile, ma quando si tratta di un concetto asincrono più complicato, la differenza tra i due può essere drastica.


1
Sembra un concetto chiave, ma non lo capisco come scritto. Ancora troppo nuovo per le promesse, immagino.
David Spector,

43

TLDR: una funzione è difficile da usare quando a volte restituisce una promessa e talvolta genera un'eccezione. Quando si scrive una funzione asincrona, preferire segnalare l'errore restituendo una promessa rifiutata

Il tuo esempio particolare offusca alcune importanti distinzioni tra di loro:

Poiché stai gestendo gli errori all'interno di una catena di promesse, le eccezioni generate vengono convertite automaticamente in promesse rifiutate. Questo potrebbe spiegare perché sembrano intercambiabili - non lo sono.

Considera la situazione seguente:

checkCredentials = () => {
    let idToken = localStorage.getItem('some token');
    if ( idToken ) {
      return fetch(`https://someValidateEndpoint`, {
        headers: {
          Authorization: `Bearer ${idToken}`
        }
      })
    } else {
      throw new Error('No Token Found In Local Storage')
    }
  }

Questo sarebbe un anti-pattern perché sarebbe quindi necessario supportare entrambi i casi di errore asincrono e sincronizzazione. Potrebbe assomigliare a:

try {
  function onFulfilled() { ... do the rest of your logic }
  function onRejected() { // handle async failure - like network timeout }
  checkCredentials(x).then(onFulfilled, onRejected);
} catch (e) {
  // Error('No Token Found In Local Storage')
  // handle synchronous failure
} 

Non va bene e qui è esattamente dove Promise.reject(disponibile nell'ambito globale) viene in soccorso e si differenzia efficacemente da throw. Il refactor ora diventa:

checkCredentials = () => {
  let idToken = localStorage.getItem('some_token');
  if (!idToken) {
    return Promise.reject('No Token Found In Local Storage')
  }
  return fetch(`https://someValidateEndpoint`, {
    headers: {
      Authorization: `Bearer ${idToken}`
    }
  })
}

Ciò ora consente di utilizzarne solo uno catch()per gli errori di rete e il controllo degli errori sincroni per mancanza di token:

checkCredentials()
      .catch((error) => if ( error == 'No Token' ) {
      // do no token modal
      } else if ( error === 400 ) {
      // do not authorized modal. etc.
      }

1
L'esempio di Op restituisce sempre una promessa, tuttavia. La domanda si riferisce al fatto se è necessario utilizzare Promise.rejecto throwquando si desidera restituire una promessa rifiutata (una promessa che passerà alla successiva .catch()).
Marcos Pereira,

@maxwell - Mi piaci il tuo esempio. Allo stesso tempo, se nel recupero aggiungerai un fermo e in esso lanci l'eccezione, allora sarai sicuro di usare prova ... cattura ... Non esiste un mondo perfetto sul flusso di eccezioni, ma penso che usando uno il modello singolo ha senso e la combinazione dei modelli non è sicura (allineata con l'analogia del modello rispetto all'analogia del modello).
user3053247

1
Risposta eccellente ma trovo qui un difetto - questo schema presuppone che tutti gli errori vengano gestiti restituendo un Promise.reject - cosa succede con tutti gli errori imprevisti che potrebbero essere generati semplicemente da checkCredentials ()?
chenop,

1
Sì hai ragione @chenop - per catturare quegli errori imprevisti che dovresti avvolgere in prova / cattura ancora
maxwell

Non capisco il caso di @ maxwell. Non potresti strutturarlo così come fai tu checkCredentials(x).then(onFulfilled).catch(e) {}e avere la catchmaniglia sia del caso di rifiuto che del caso di errore generato?
Ben Wheeler,

5

Un esempio da provare. Cambia isVersionThrow in false per usare il rifiuto invece del lancio.

const isVersionThrow = true

class TestClass {
  async testFunction () {
    if (isVersionThrow) {
      console.log('Throw version')
      throw new Error('Fail!')
    } else {
      console.log('Reject version')
      return new Promise((resolve, reject) => {
        reject(new Error('Fail!'))
      })
    }
  }
}

const test = async () => {
  const test = new TestClass()
  try {
    var response = await test.testFunction()
    return response 
  } catch (error) {
    console.log('ERROR RETURNED')
    throw error 
  }  
}

test()
.then(result => {
  console.log('result: ' + result)
})
.catch(error => {
  console.log('error: ' + 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.