Jak odrzucić w składni asynchronicznej / oczekującej?

282

Jak mogę odrzucić obietnicę zwróconą przez funkcję asynchroniczną / oczekującą?

np. pierwotnie

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

Przetłumacz na async / czekaj

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

Jak mogę właściwie odrzucić tę obietnicę w tym przypadku?

Feniks
źródło
20
Unikaj Promisekonstruktora antipattern ! Nawet pierwszy fragment powinien zostać napisanyfoo(id: string): Promise<A> { return someAsyncPromise().then(()=>{ return 200; }, ()=>{ throw 400; }); }
Bergi
10
Myślę, że pomocne byłoby przetłumaczenie kodu w tym pytaniu na waniliowy JS, ponieważ pytanie to nie ma nic wspólnego z TypeScript. Gdybym to zrobił, czy ta edycja prawdopodobnie zostałaby zaakceptowana?
Jacob Ford

Odpowiedzi:

328

Najprościej jest throww Erroropakowaniu wartość, co powoduje przerwanie obietnicy z Erroropakowania wartość:

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

Możesz także podać tylko throwwartość, ale wtedy nie ma informacji o śledzeniu stosu:

} catch (error) {
    throw 400;
}

Alternatywnie, zwróć odrzuconą obietnicę z Errorzawijaniem wartości, ale nie jest to idiomatyczne:

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

(Lub po prostu return Promise.reject(400);, ale znowu, wtedy nie ma informacji kontekstowych.)

(W twoim przypadku, gdy używasz, TypeScripta foowartość pobierana to Promise<A>, należy użyć return Promise.reject<A>(400 /*or error*/);)

W sytuacji async/ awaitto ostatnie jest prawdopodobnie semantycznym niedopasowaniem, ale działa.

Jeśli rzucisz an Error, to gra dobrze z czymkolwiek, co pochłania foowynik w awaitskładni:

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
źródło
12
A ponieważ async / await polega na przywróceniu przepływu asynchronicznego z powrotem do synchronizacji składni, throwjest lepszy niż Promise.reject()IMO. Czy throw 400to inne pytanie. W PO odrzuca 400, a my możemy argumentować, że Errorzamiast tego powinien odrzucić .
nieobowiązkowe
2
Tak, jednak jeśli twój łańcuch kodu naprawdę używa asynchronizacji / czekania, to ... trudno będzie tutaj wpisać, pozwól, że
pokażę
1
czy jest jakiś powód, dla którego chcesz zgłosić nowy błąd, a nie błąd podany w bloku catch?
Adrian M
1
@sebastian - Nie wiem, co masz na myśli. W asyncfunkcjach nie ma resolvelub nie ma rejectfunkcji. Są returni throw, które są idiomatycznymi sposobami rozwiązania i odrzucenia asyncobietnicy funkcji.
TJ Crowder,
1
@ Jan-PhilipGehrcke - You can , ale nigdy nie robić. To tworzenie instancji, newto wyraźnie. Pamiętaj też, że nie możesz tego pominąć, jeśli masz Errorpodklasę ( class MyError extends Error), więc ...
TJ Crowder
146

Prawdopodobnie należy również wspomnieć, że można po prostu catch()połączyć funkcję po wywołaniu operacji asynchronicznej, ponieważ pod maską nadal jest zwracana obietnica.

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

W ten sposób możesz uniknąć try/catchskładni, jeśli Ci się nie podoba.

David
źródło
1
Więc jeśli chcę odrzucić moją asyncfunkcję, rzucam wyjątek, a następnie ładnie go łapię, .catch()tak jak gdybym wrócił Promise.rejectlub zadzwonił reject. Lubię to!
icl7126
7
Nie rozumiem, dlaczego to powinna być zaakceptowana odpowiedź. Akceptowana odpowiedź jest nie tylko czystsza, ale także obsługuje wszystkie możliwe awaitbłędy w jednej procedurze. O ile nie są potrzebne bardzo konkretne przypadki dla każdego await, nie rozumiem, dlaczego chcesz je tak złapać. Tylko ja pokorna opinia.
edgaralienfoe
1
@jablesauce dla mojego przypadku użycia, nie tylko musiałem wychwycić każdą awaitawarię osobno, ale także musiałem pracować z ramami opartymi na obietnicach, które odrzucały obietnice błędu.
Reuven Karasik
Nie działało to dla mnie. Nie wydaje się wchodzić w blok catch, jeśli URL zawiedzie. [odpowiedź] = czekaj na oauthGet ( ${host}/user/permissions/repositories_wrong_url/, accessToken, accessTokenSecret) .catch (err => {logger.error ('Nie można pobrać uprawnień do repozytorium', err); callback (err);})
sn.anurag
1
nie potrzebuje awaittutaj słowa kluczowego.
Ashish Rawat
12

Możesz utworzyć funkcję opakowania, która przyjmuje obietnicę i zwraca tablicę z danymi, jeśli nie ma błędu, i błędem, jeśli wystąpił błąd.

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

Użyj tego w ES7 i w funkcji asynchronicznej :

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
źródło
1
Wygląda na próbę posiadania pięknej składni Go, ale bez dużej elegancji. Uważam, że kod, który go używa, jest zaciemniony na tyle, aby wyssać wartość z rozwiązania.
Kim
8

Lepszym sposobem na napisanie funkcji asynchronicznej byłoby zwrócenie oczekującej obietnicy od początku, a następnie obsługa zarówno odrzucania, jak i rozstrzygania w ramach wywołania zwrotnego obietnicy, zamiast tylko wyrzucania odrzuconej obietnicy w przypadku błędu. Przykład:

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

Następnie po prostu połącz metody ze zwróconej obietnicy:

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

bar()

Źródło - ten samouczek:

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

OzzyTheGiant
źródło
5
Pytanie konkretnie zadane na temat używania asynchronizacji / oczekiwania. Niewykorzystywanie obietnic
Mak
Ta odpowiedź nie miała być ostateczną poprawną odpowiedzią. To była odpowiedź wspierająca na inne odpowiedzi podane powyżej. Odłożyłbym to jako komentarz, ale biorąc pod uwagę, że mam kod, pole odpowiedzi jest lepszym miejscem.
OzzyTheGiant
Dzięki za wytłumaczenie. Zdecydowanie pomocne jest pokazanie, jak wykonać funkcję asynchroniczną. Aktualizacja drugiego bloku kodu do użycia w oczekiwaniu będzie o wiele bardziej trafna i przydatna. Pozdrawiam
Mak
Zredagowałem twoją odpowiedź, aby ją zaktualizować. Daj mi znać, jeśli coś przeoczyłem
Mak
4

Mam sugestię, aby właściwie obsługiwać odrzucenia w nowatorskim podejściu, bez posiadania wielu bloków try-catch.

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

Skąd należy importować funkcję to.ts :

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

Kredyty trafiają do Dimy Grossman pod poniższym linkiem .

Pedro Lourenço
źródło
1
Używam tej konstrukcji prawie wyłącznie (znacznie czystsze) i istnieje moduł „do”, który istnieje już jakiś czas npmjs.com/package/await-to-js . Nie potrzebujesz osobnej deklaracji, po prostu umieść przed zdekonstruowanym zadaniem. Można to zrobić tylko let [err]=wtedy, gdy sprawdzane są błędy.
DKebler
3

To nie jest odpowiedź na odpowiedź @TJ Crowder. Po prostu komentarz odpowiadający na komentarz „I faktycznie, jeśli wyjątek zostanie przekształcony w odrzucenie, nie jestem pewien, czy tak naprawdę przeszkadza mi, jeśli jest to Błąd. Moje powody rzucania tylko błędu prawdopodobnie nie mają zastosowania. „

jeśli twój kod używa async/ await, nadal dobrą praktyką jest odrzucanie za pomocą Errorzamiast 400:

try {
  await foo('a');
}
catch (e) {
  // you would still want `e` to be an `Error` instead of `400`
}
Unional
źródło
3

Wiem, że jest to stare pytanie, ale natknąłem się na wątek i wydaje się, że istnieje tutaj związek między błędami a odrzuceniem, który działa w wielu przypadkach (przynajmniej w wielu przypadkach) często powtarzanej porady, aby nie używać obsługi wyjątków do zajmować się przewidywanymi przypadkami. Przykład: jeśli metoda asynchroniczna próbuje uwierzytelnić użytkownika, a uwierzytelnienie nie powiedzie się, jest to odrzucenie (jeden z dwóch przewidywanych przypadków), a nie błąd (np. Jeśli interfejs API uwierzytelniania był niedostępny).

Aby upewnić się, że nie tylko dzielę włosy, przeprowadziłem test wydajności trzech różnych podejść, używając tego kodu:

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

Niektóre rzeczy, które tam są, są zawarte z powodu mojej niepewności co do interpretera Javascript (lubię schodzić tylko do jednej dziury królika na raz); na przykład, zawarłem doSomethingfunkcję i przypisałem jej zwrot, aby dummyValuemieć pewność, że bloki warunkowe nie zostaną zoptymalizowane.

Moje wyniki to:

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

Wiem, że istnieje wiele przypadków, w których polowanie na małe optymalizacje nie jest warte kłopotu, ale w systemach na większą skalę te rzeczy mogą mieć dużą kumulatywną różnicę, i jest to dość surowe porównanie.

SO… chociaż myślę, że podejście przyjęte w odpowiedzi jest słuszne w przypadkach, w których oczekujesz, że będziesz musiał poradzić sobie z nieprzewidywalnymi błędami w funkcji asynchronicznej, w przypadkach, gdy odrzucenie oznacza po prostu „musisz przejść do planu B (lub C lub D…) „Myślę, że preferuję odrzucenie przy użyciu niestandardowego obiektu odpowiedzi.

RiqueW
źródło
2
Pamiętaj również, że nie musisz stresować się obsługą nieprzewidzianych błędów w funkcji asynchronicznej, jeśli wywołanie tej funkcji znajduje się w bloku try / catch w otaczającym zakresie, ponieważ - w przeciwieństwie do obietnic - funkcje asynchroniczne wyrzucają swoje błędy do obejmujący zakres, w którym są obsługiwane tak samo, jak błędy lokalne dla tego zakresu. To jeden z głównych atutów asynchronizacji / czekania!
RiqueW
Mikrodrobności to diabeł. Przyjrzyj się bliżej liczbom. Musisz zrobić coś 1000x, aby zauważyć różnicę 1ms tutaj. Tak, dodanie rzut / catch dezoptymalizuje funkcję. Ale a) jeśli czekasz na coś asynchronicznego, prawdopodobnie zajmie to kilka rzędów wielkości dłużej niż 0,0005 ms w tle. b) musisz zrobić to 1000 razy, aby zrobić różnicę 1ms tutaj.
Jamie Pate,