Wie kann ich in asynchroner / wartender Syntax ablehnen?

282

Wie kann ich ein Versprechen ablehnen, das von einer asynchronen / wartenden Funktion zurückgegeben wurde?

zB ursprünglich

foo(id: string): Promise<A> {
  return new Promise((resolve, reject) => {
    someAsyncPromise().then((value)=>resolve(200)).catch((err)=>reject(400))
  });
}

In async / await übersetzen

async foo(id: string): Promise<A> {
  try{
    await someAsyncPromise();
    return 200;
  } catch(error) {//here goes if someAsyncPromise() rejected}
    return 400; //this will result in a resolved promise.
  });
}

Wie könnte ich dieses Versprechen in diesem Fall richtig ablehnen?

Phönix
quelle
20
Vermeiden Sie das PromiseKonstruktor-Antimuster ! Sogar der erste Ausschnitt sollte geschrieben worden seinfoo(id: string): Promise<A> { return someAsyncPromise().then(()=>{ return 200; }, ()=>{ throw 400; }); }
Bergi
10
Ich denke, es wäre hilfreich, den Code in dieser Frage in Vanilla JS zu übersetzen, da die Frage nichts mit TypeScript zu tun hat. Wenn ich das tun würde, würde diese Änderung wahrscheinlich akzeptiert werden?
Jacob Ford

Antworten:

326

Ihre beste Wette ist , um throweine ErrorVerpackung der Wert, der zu einer abgelehnten Versprechen mit einer ErrorUmhüllung des Wert:

} catch (error) {
    throw new Error(400);
}

Sie können auch nur throwden Wert angeben, aber dann gibt es keine Informationen zur Stapelverfolgung:

} catch (error) {
    throw 400;
}

Alternativ können Sie ein abgelehntes Versprechen mit einer ErrorUmhüllung des Werts zurückgeben, dies ist jedoch nicht idiomatisch:

} catch (error) {
    return Promise.reject(new Error(400));
}

(Oder einfach return Promise.reject(400);, aber auch dann gibt es keine Kontextinformationen.)

(In Ihrem Fall, wenn Sie verwenden TypeScriptund fooder Retrn-Wert ist Promise<A>, würden Sie verwenden return Promise.reject<A>(400 /*or error*/);)

In einer async/ awaitSituation ist das letzte wahrscheinlich ein bisschen eine semantische Fehlanpassung, aber es funktioniert.

Wenn Sie einen werfen Error, spielt das gut mit allem, was Ihr fooErgebnis mit awaitSyntax verbraucht :

try {
    await foo();
} catch (error) {
    // Here, `error` would be an `Error` (with stack trace, etc.).
    // Whereas if you used `throw 400`, it would just be `400`.
}
TJ Crowder
quelle
12
Und da es bei async / await darum geht, den asynchronen Fluss zurück zur Synchronisationssyntax zu bringen, throwist dies besser als Promise.reject()IMO. Ob zu throw 400ist eine andere Frage. Im OP lehnt es 400 ab, und wir können argumentieren, dass es Errorstattdessen ein ablehnen sollte .
unional
2
Ja, aber wenn Ihre Codekette wirklich async / await verwendet, dann werden Sie ..... zu schwer hier zu tippen, lassen Sie mich als Antwort Demo
unional
1
Gibt es einen Grund, warum Sie einen neuen Fehler auslösen möchten, im Gegensatz zu dem Fehler, den Sie im catch-Block erhalten haben?
Adrian M
1
@sebastian - Ich weiß nicht, was du dort meinst. In asyncFunktionen gibt es keine resolveoder rejectFunktion. Es gibt returnund throw, die die idiomatischen Möglichkeiten sind, asyncdas Versprechen der Funktion aufzulösen und abzulehnen .
TJ Crowder
1
@ Jan-PhilipGehrcke - Du kannst , aber ich mache es nie. Es erstellt eine Instanz, newmacht dies explizit. Beachten Sie auch, dass Sie es nicht Errorclass MyError extends Error
TJ Crowder
146

Es sollte wahrscheinlich auch erwähnt werden, dass Sie eine catch()Funktion nach dem Aufruf Ihrer asynchronen Operation einfach verketten können, da unter der Haube noch ein Versprechen zurückgegeben wird.

await foo().catch(error => console.log(error));

Auf diese Weise können Sie die try/catchSyntax vermeiden, wenn Sie sie nicht mögen.

David
quelle
1
Wenn ich also meine asyncFunktion ablehnen möchte, löse ich eine Ausnahme und fange sie dann gut ein, .catch()genau wie wenn ich zurückgekommen bin Promise.rejectoder angerufen habe reject. Ich mag das!
icl7126
7
Ich verstehe nicht, warum dies die akzeptierte Antwort sein sollte. Die akzeptierte Antwort ist nicht nur sauberer, sondern behandelt auch alle möglichen awaitFehler in einer Routine. Sofern nicht für jeden Fall sehr spezielle Fälle erforderlich sind, awaitverstehe ich nicht, warum Sie sie so fangen möchten. Nur ich bescheidene Meinung.
Edgaralienfoe
1
@jablesauce Für meinen Anwendungsfall musste ich nicht nur jeden awaitFehler separat abfangen , sondern auch mit einem Promise-basierten Framework arbeiten, das die Versprechen auf Fehler ablehnte.
Reuven Karasik
Bei mir hat es nicht funktioniert. Scheint nicht im Catch-Block zu sein, wenn die URL fehlschlägt. [response] = warte auf oauthGet ( ${host}/user/permissions/repositories_wrong_url/, accessToken, accessTokenSecret) .catch (err => {logger.error ('Repository-Berechtigungen können nicht abgerufen werden', err); Rückruf (err);})
sn.anurag
1
benötigt awaithier kein Schlüsselwort.
Ashish Rawat
12

Sie können eine Wrapper-Funktion erstellen , die ein Versprechen einnimmt und ein Array mit Daten zurückgibt, wenn kein Fehler vorliegt, und den Fehler, wenn ein Fehler aufgetreten ist.

function safePromise(promise) {
  return promise.then(data => [ data ]).catch(error => [ null, error ]);
}

Verwenden Sie es so in ES7 und in einer asynchronen Funktion:

async function checkItem() {
  const [ item, error ] = await safePromise(getItem(id));
  if (error) { return null; } // handle error and return
  return item; // no error so safe to use item
}
Andy
quelle
1
Sieht aus wie ein Versuch, die schöne Go-Syntax zu haben, aber ohne viel Eleganz. Ich finde, dass der Code, der ihn verwendet, gerade genug verschleiert ist, um den Wert aus der Lösung herauszuholen.
Kim
8

Eine bessere Möglichkeit, die asynchrone Funktion zu schreiben, besteht darin, ein ausstehendes Versprechen von Anfang an zurückzugeben und dann sowohl Ablehnungen als auch Auflösungen innerhalb des Rückrufs des Versprechens zu behandeln, anstatt nur ein abgelehntes Versprechen auf Fehler auszuspucken. Beispiel:

async foo(id: string): Promise<A> {
    return new Promise(function(resolve, reject) {
        // execute some code here
        if (success) { // let's say this is a boolean value from line above
            return resolve(success);
        } else {
            return reject(error); // this can be anything, preferably an Error object to catch the stacktrace from this function
        }
    });
}

Dann verketten Sie einfach die Methoden für das zurückgegebene Versprechen:

async function bar () {
    try {
        var result = await foo("someID")
        // use the result here
    } catch (error) {
        // handle error here
    }
}

bar()

Quelle - dieses Tutorial:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise

OzzyTheGiant
quelle
5
Die Frage wurde speziell zur Verwendung von async / await gestellt. Keine Versprechen verwenden
Mak
Diese Antwort sollte nicht die endgültige richtige Antwort sein. Dies war eine unterstützende Antwort auf die anderen oben angegebenen Antworten. Ich hätte es als Kommentar notiert, aber da ich Code habe, ist das Antwortfeld ein besserer Ort.
OzzyTheGiant
Danke fürs klarstellen. Es ist auf jeden Fall hilfreich zu zeigen, wie eine asynchrone Funktion erstellt wird. Das Aktualisieren des zweiten zu wartenden Codeblocks ist viel relevanter und nützlicher. Prost
Mak
Ich habe Ihre Antwort bearbeitet, um sie zu aktualisieren. Lassen Sie mich wissen, wenn ich etwas verpasst habe
Mak
4

Ich habe einen Vorschlag, Ablehnungen in einem neuartigen Ansatz richtig zu behandeln , ohne mehrere Try-Catch-Blöcke zu haben.

import to from './to';

async foo(id: string): Promise<A> {
    let err, result;
    [err, result] = await to(someAsyncPromise()); // notice the to() here
    if (err) {
        return 400;
    }
    return 200;
}

Woher die Funktion to.ts importiert werden soll:

export default function to(promise: Promise<any>): Promise<any> {
    return promise.then(data => {
        return [null, data];
    }).catch(err => [err]);
}

Credits gehen an Dima Grossman unter folgendem Link .

Pedro Lourenço
quelle
1
Ich benutze diese Konstruktion fast ausschließlich (viel sauberer) und es gibt ein ' to' -Modul, das es schon eine Weile gibt npmjs.com/package/await-to-js . Sie müssen die separate Deklaration nicht einfach vor die dekonstruierte Zuordnung stellen. Kann auch nur tun, let [err]=wenn nur auf Fehler geprüft wird.
DKebler
3

Dies ist keine Antwort über die von @TJ Crowder. Nur ein Kommentar, der auf den Kommentar antwortet "Und tatsächlich, wenn die Ausnahme in eine Ablehnung umgewandelt werden soll, bin ich mir nicht sicher, ob es mich wirklich stört, wenn es sich um einen Fehler handelt. Meine Gründe, nur Fehler auszulösen, treffen wahrscheinlich nicht zu. ""

Wenn Ihr Code async/ verwendet await, ist es immer noch eine gute Praxis, mit einem Errorstatt mit 400: abzulehnen.

try {
  await foo('a');
}
catch (e) {
  // you would still want `e` to be an `Error` instead of `400`
}
unional
quelle
3

Ich weiß, dass dies eine alte Frage ist, aber ich bin gerade über den Thread gestolpert und es scheint hier eine Verschmelzung zwischen Fehlern und Ablehnung zu geben, die (zumindest in vielen Fällen) gegen den oft wiederholten Rat verstößt, keine Ausnahmebehandlung zu verwenden sich mit erwarteten Fällen befassen. Zur Veranschaulichung: Wenn eine asynchrone Methode versucht, einen Benutzer zu authentifizieren, und die Authentifizierung fehlschlägt, ist dies eine Ablehnung (einer von zwei erwarteten Fällen) und kein Fehler (z. B. wenn die Authentifizierungs-API nicht verfügbar war).

Um sicherzustellen, dass ich nicht nur Haare spalte, habe ich mit diesem Code einen Leistungstest mit drei verschiedenen Ansätzen durchgeführt:

const iterations = 100000;

function getSwitch() {
  return Math.round(Math.random()) === 1;
}

function doSomething(value) {
  return 'something done to ' + value.toString();
}

let processWithThrow = function () {
  if (getSwitch()) {
    throw new Error('foo');
  }
};

let processWithReturn = function () {
  if (getSwitch()) {
    return new Error('bar');
  } else {
    return {}
  }
};

let processWithCustomObject = function () {
  if (getSwitch()) {
    return {type: 'rejection', message: 'quux'};
  } else {
    return {type: 'usable response', value: 'fnord'};
  }
};

function testTryCatch(limit) {
  for (let i = 0; i < limit; i++) {
    try {
      processWithThrow();
    } catch (e) {
      const dummyValue = doSomething(e);
    }
  }
}

function testReturnError(limit) {
  for (let i = 0; i < limit; i++) {
    const returnValue = processWithReturn();
    if (returnValue instanceof Error) {
      const dummyValue = doSomething(returnValue);
    }
  }
}

function testCustomObject(limit) {
  for (let i = 0; i < limit; i++) {
    const returnValue = processWithCustomObject();
    if (returnValue.type === 'rejection') {
      const dummyValue = doSomething(returnValue);
    }
  }
}

let start, end;
start = new Date();
testTryCatch(iterations);
end = new Date();
const interval_1 = end - start;
start = new Date();
testReturnError(iterations);
end = new Date();
const interval_2 = end - start;
start = new Date();
testCustomObject(iterations);
end = new Date();
const interval_3 = end - start;

console.log(`with try/catch: ${interval_1}ms; with returned Error: ${interval_2}ms; with custom object: ${interval_3}ms`);

Einige der Dinge, die dort drin sind, sind aufgrund meiner Unsicherheit bezüglich des Javascript-Interpreters enthalten (ich gehe immer nur ein Kaninchenloch auf einmal hinunter); Zum Beispiel habe ich die aufgenommendoSomething Funktion eingeschlossen und ihre Rückgabe zugewiesen, dummyValueum sicherzustellen, dass die bedingten Blöcke nicht optimiert werden.

Meine Ergebnisse waren:

with try/catch: 507ms; with returned Error: 260ms; with custom object: 5ms

Ich weiß, dass es viele Fälle gibt, in denen es sich nicht lohnt, nach kleinen Optimierungen zu suchen, aber in größeren Systemen können diese Dinge einen großen kumulativen Unterschied bewirken, und das ist ein ziemlich starker Vergleich.

SO… während ich denke, dass der Ansatz der akzeptierten Antwort in Fällen, in denen Sie erwarten, unvorhersehbare Fehler innerhalb einer asynchronen Funktion behandeln zu müssen, vernünftig ist, in Fällen, in denen eine Ablehnung einfach bedeutet, dass Sie sich für Plan B entscheiden müssen (oder C oder D…) "Ich denke, ich würde es vorziehen, die Verwendung eines benutzerdefinierten Antwortobjekts abzulehnen.

RiqueW
quelle
2
Denken Sie auch daran, dass Sie sich nicht über die Behandlung unerwarteter Fehler innerhalb einer asynchronen Funktion Gedanken machen müssen, wenn sich der Aufruf dieser Funktion innerhalb eines Try / Catch-Blocks im umschließenden Bereich befindet, da asynchrone Funktionen - im Gegensatz zu Promises - ihre ausgelösten Fehler an die umschließenden Bereich, in dem sie wie lokale Fehler behandelt werden. Das ist einer der Hauptvorteile von Async / Warten!
RiqueW
Mikrobenchmarks sind der Teufel. Schauen Sie sich die Zahlen genauer an. Sie müssen 1000x etwas tun, um hier einen Unterschied von 1 ms zu bemerken. Ja, durch Hinzufügen von throw / catch wird die Funktion deoptimiert. Aber a) Wenn Sie auf etwas Asynchrones warten, dauert es wahrscheinlich mehrere Größenordnungen länger als 0,0005 Ms, bis es im Hintergrund passiert. b) Sie müssen es 1000x machen, um hier einen Unterschied von 1 ms zu machen.
Jamie Pate