Lepsza obsługa błędów w NodeJS z klasami błędów

Opublikowany: 2022-03-10
Krótkie podsumowanie ↬ Ten artykuł jest przeznaczony dla programistów JavaScript i NodeJS, którzy chcą poprawić obsługę błędów w swoich aplikacjach. Kelvin Omereshone wyjaśnia wzorzec klasy error i jak go używać, aby uzyskać lepszy i wydajniejszy sposób obsługi błędów w aplikacjach.

Obsługa błędów jest jedną z tych części tworzenia oprogramowania, które nie poświęcają tyle uwagi, na ile naprawdę zasługują. Jednak budowanie solidnych aplikacji wymaga odpowiedniego radzenia sobie z błędami.

Możesz poradzić sobie z NodeJS bez prawidłowej obsługi błędów, ale ze względu na asynchroniczną naturę NodeJS niewłaściwa obsługa lub błędy mogą wkrótce spowodować ból — zwłaszcza podczas debugowania aplikacji.

Zanim przejdziemy dalej, chciałbym wskazać rodzaje błędów, które będziemy omawiać, jak wykorzystać klasy błędów.

Błędy operacyjne

Są to błędy wykryte w czasie wykonywania programu. Błędy operacyjne nie są błędami i mogą pojawiać się od czasu do czasu, głównie z powodu jednego lub kombinacji kilku czynników zewnętrznych, takich jak przekroczenie limitu czasu serwera bazy danych lub decyzja użytkownika o podjęciu próby wstrzyknięcia SQL poprzez wprowadzenie zapytań SQL w polu wejściowym.

Poniżej więcej przykładów błędów operacyjnych:

  • Nie udało się połączyć z serwerem bazy danych;
  • Nieprawidłowe dane wprowadzone przez użytkownika (serwer odpowiada kodem odpowiedzi 400 );
  • Limit czasu żądania;
  • Nie znaleziono zasobu (serwer odpowiada kodem odpowiedzi 404);
  • Serwer powraca z odpowiedzią 500 .

Warto również krótko omówić odpowiednik błędów operacyjnych.

Błędy programisty

Są to błędy w programie, które można naprawić zmieniając kod. Tego typu błędy nie mogą być obsługiwane, ponieważ pojawiają się w wyniku złamania kodu. Przykładami tych błędów są:

  • Próba odczytania właściwości obiektu, który nie jest zdefiniowany.
 const user = { firstName: 'Kelvin', lastName: 'Omereshone', } console.log(user.fullName) // throws 'undefined' because the property fullName is not defined
  • Wywoływanie lub wywoływanie funkcji asynchronicznej bez wywołania zwrotnego.
  • Przekazywanie ciągu, w którym oczekiwano liczby.

Ten artykuł dotyczy obsługi błędów operacyjnych w NodeJS. Obsługa błędów w NodeJS znacznie różni się od obsługi błędów w innych językach. Wynika to z asynchronicznej natury JavaScript i otwartości JavaScript z błędami. Pozwól mi wyjaśnić:

W JavaScript instancje klasy error nie są jedyną rzeczą, którą możesz rzucić. Możesz dosłownie wrzucić dowolny typ danych, na taką otwartość nie pozwalają inne języki.

Na przykład programista JavaScript może zdecydować się na wrzucenie liczby zamiast instancji obiektu błędu, jak na przykład:

 // bad throw 'Whoops :)'; // good throw new Error('Whoops :)')

Możesz nie widzieć problemu z wyrzucaniem innych typów danych, ale spowoduje to trudniejsze debugowanie, ponieważ nie otrzymasz śladu stosu i innych właściwości, które uwidacznia obiekt Error, które są potrzebne do debugowania.

Przyjrzyjmy się niektórym niepoprawnym wzorcom w obsłudze błędów, zanim przyjrzymy się wzorcowi klasy Error i jak jest to znacznie lepszy sposób obsługi błędów w NodeJS.

Więcej po skoku! Kontynuuj czytanie poniżej ↓

Zły wzorzec obsługi błędów nr 1: niewłaściwe użycie wywołań zwrotnych

Scenariusz rzeczywisty : Twój kod zależy od zewnętrznego interfejsu API wymagającego wywołania zwrotnego w celu uzyskania oczekiwanego wyniku.

Weźmy poniższy fragment kodu:

 'use strict'; const fs = require('fs'); const write = function () { fs.mkdir('./writeFolder'); fs.writeFile('./writeFolder/foobar.txt', 'Hello World'); } write();

Do NodeJS 8 i nowszych powyższy kod był legalny, a programiści po prostu uruchamiali i zapominali polecenia. Oznacza to, że programiści nie byli zobowiązani do dostarczania wywołań zwrotnych do takich wywołań funkcji, a zatem mogli pominąć obsługę błędów. Co się stanie, gdy writeFolder nie został utworzony? Wywołanie writeFile nie zostanie wykonane i nic o tym nie wiedzielibyśmy. Może to również skutkować wyścigiem, ponieważ pierwsze polecenie mogło się nie skończyć, gdy drugie polecenie rozpoczęło się ponownie, nie wiedziałbyś.

Zacznijmy rozwiązywać ten problem od rozwiązania warunku wyścigu. Zrobilibyśmy to poprzez wywołanie zwrotne do pierwszego polecenia mkdir , aby upewnić się, że katalog rzeczywiście istnieje przed zapisaniem do niego za pomocą drugiego polecenia. Więc nasz kod wyglądałby jak ten poniżej:

 'use strict'; const fs = require('fs'); const write = function () { fs.mkdir('./writeFolder', () => { fs.writeFile('./writeFolder/foobar.txt', 'Hello World!'); }); } write();

Chociaż rozwiązaliśmy warunek wyścigu, jeszcze nie skończyliśmy. Nasz kod nadal jest problematyczny, ponieważ chociaż użyliśmy wywołania zwrotnego dla pierwszego polecenia, nie mamy możliwości sprawdzenia, czy folder writeFolder został utworzony, czy nie. Jeśli folder nie został utworzony, drugie wywołanie ponownie się nie powiedzie, ale nadal zignorowaliśmy błąd po raz kolejny. Rozwiązujemy to poprzez…

Obsługa błędów z wywołaniami zwrotnymi

Aby poprawnie obsłużyć błąd za pomocą wywołań zwrotnych, musisz upewnić się, że zawsze stosujesz podejście „najpierw błąd”. Oznacza to, że powinieneś najpierw sprawdzić, czy funkcja nie zwróciła błędu, zanim zaczniesz korzystać z wszelkich zwróconych danych (jeśli takie istnieją). Zobaczmy, jak to zrobić w niewłaściwy sposób:

 'use strict'; // Wrong const fs = require('fs'); const write = function (callback) { fs.mkdir('./writeFolder', (err, data) => { if (data) fs.writeFile('./writeFolder/foobar.txt', 'Hello World!'); else callback(err) }); } write(console.log);

Powyższy wzorzec jest błędny, ponieważ czasami wywołany interfejs API może nie zwracać żadnej wartości lub może zwracać fałszywą wartość jako prawidłową wartość zwracaną. To spowodowałoby, że skończysz w przypadku błędu, nawet jeśli prawdopodobnie masz pomyślne wywołanie funkcji lub interfejsu API.

Powyższy wzorzec jest również zły, ponieważ jego użycie pochłonie twój błąd (twoje błędy nie zostaną wywołane, nawet jeśli mogło się to wydarzyć). Nie będziesz też miał pojęcia, co dzieje się w twoim kodzie w wyniku tego rodzaju wzorca obsługi błędów. Więc właściwym sposobem na powyższy kod będzie:

 'use strict'; // Right const fs = require('fs'); const write = function (callback) { fs.mkdir('./writeFolder', (err, data) => { if (err) return callback(err) fs.writeFile('./writeFolder/foobar.txt', 'Hello World!'); }); } write(console.log);

Niewłaściwy wzorzec obsługi błędów nr 2: Niewłaściwe wykorzystanie obietnic

Scenariusz ze świata rzeczywistego : Więc odkryłeś Promises i myślisz, że są one o wiele lepsze niż wywołania zwrotne z powodu piekła wywołań zwrotnych i zdecydowałeś się obiecać jakiś zewnętrzny interfejs API, od którego zależała twoja baza kodu. Lub zużywasz obietnicę z zewnętrznego interfejsu API lub interfejsu API przeglądarki, takiego jak funkcja fetch().

W dzisiejszych czasach tak naprawdę nie używamy wywołań zwrotnych w naszych bazach kodu NodeJS, używamy obietnic. Zaimplementujmy więc nasz przykładowy kod obietnicą:

 'use strict'; const fs = require('fs').promises; const write = function () { return fs.mkdir('./writeFolder').then(() => { fs.writeFile('./writeFolder/foobar.txt', 'Hello world!') }).catch((err) => { // catch all potential errors console.error(err) }) }

Umieśćmy powyższy kod pod mikroskopem — widzimy, że rozgałęziamy obietnicę fs.mkdir do innego łańcucha obietnic (wywołanie fs.writeFile) nawet bez obsługi tego wywołania obietnicy. Możesz pomyśleć, że lepszym sposobem na zrobienie tego byłoby:

 'use strict'; const fs = require('fs').promises; const write = function () { return fs.mkdir('./writeFolder').then(() => { fs.writeFile('./writeFolder/foobar.txt', 'Hello world!').then(() => { // do something }).catch((err) => { console.error(err); }) }).catch((err) => { // catch all potential errors console.error(err) }) }

Ale powyższe nie skalowałoby się. Dzieje się tak dlatego, że gdybyśmy mieli więcej łańcucha obietnic do sprawdzenia, skończylibyśmy z czymś podobnym do piekła zwrotnego, którego rozwiązanie zostało złożone z obietnicami. Oznacza to, że nasz kod będzie nadal wcinał się z prawej strony. Mielibyśmy na rękach piekło obietnicy.

Obiecywanie API opartego na wywołaniach zwrotnych

W większości przypadków chcesz samodzielnie obiecać interfejs API oparty na wywołaniach zwrotnych, aby lepiej radzić sobie z błędami w tym interfejsie API. Nie jest to jednak łatwe. Weźmy przykład poniżej, aby wyjaśnić dlaczego.

 function doesWillNotAlwaysSettle(arg) { return new Promise((resolve, reject) => { doATask(foo, (err) => { if (err) { return reject(err); } if (arg === true) { resolve('I am Done') } }); }); }

Z powyższego, jeśli arg nie jest true i nie mamy błędu z wywołania funkcji doATask , to ta obietnica po prostu się zawiesi, co jest wyciekiem pamięci w twojej aplikacji.

Błędy połkniętej synchronizacji w obietnicach

Korzystanie z konstruktora Promise ma kilka trudności, jedną z nich jest; gdy tylko zostanie rozwiązany lub odrzucony, nie może uzyskać innego stanu. Dzieje się tak, ponieważ obietnica może uzyskać tylko jeden stan — albo jest w toku, albo jest rozwiązana/odrzucona. Oznacza to, że w naszych obietnicach możemy mieć martwe strefy. Zobaczmy to w kodzie:

 function deadZonePromise(arg) { return new Promise((resolve, reject) => { doATask(foo, (err) => { resolve('I'm all Done'); throw new Error('I am never reached') // Dead Zone }); }); }

Z powyższego widzimy, jak tylko obietnica zostanie rozwiązana, następna linia jest martwą strefą i nigdy nie zostanie osiągnięta. Oznacza to, że każda następująca synchroniczna obsługa błędów w Twoich obietnicach zostanie po prostu połknięta i nigdy nie zostanie wyrzucona.

Przykłady ze świata rzeczywistego

Powyższe przykłady pomagają wyjaśnić złe wzorce obsługi błędów, przyjrzyjmy się rodzajom problemów, które możesz zobaczyć w prawdziwym życiu.

Przykład 1 ze świata rzeczywistego — przekształcenie błędu w ciąg

Scenariusz : Zdecydowałeś, że błąd zwrócony przez interfejs API nie jest dla Ciebie wystarczająco dobry, więc zdecydowałeś się dodać do niego własną wiadomość.

 'use strict'; function readTemplate() { return new Promise(() => { databaseGet('query', function(err, data) { if (err) { reject('Template not found. Error: ', + err); } else { resolve(data); } }); }); } readTemplate();

Przyjrzyjmy się, co jest nie tak z powyższym kodem. Z powyższego widzimy, że programista próbuje poprawić błąd zgłoszony przez databaseGet danychGet API, łącząc zwrócony błąd z ciągiem „Nie znaleziono szablonu”. Takie podejście ma wiele wad, ponieważ po zakończeniu łączenia programista niejawnie uruchamia toString na zwróconym obiekcie błędu. W ten sposób traci wszelkie dodatkowe informacje zwrócone przez błąd (pożegnaj się ze śladem stosu). Więc to, co teraz ma programista, to tylko ciąg znaków, który nie jest przydatny podczas debugowania.

Lepszym sposobem jest zachowanie błędu bez zmian lub umieszczenie go w innym błędzie, który utworzyłeś i dołączyłeś zgłoszony błąd z wywołania databaseGet jako jego właściwość.

Przykład z życia wzięty nr 2: Całkowite zignorowanie błędu

Scenariusz : być może, gdy użytkownik rejestruje się w Twojej aplikacji, jeśli wystąpi błąd, chcesz po prostu przechwycić błąd i wyświetlić niestandardową wiadomość, ale całkowicie zignorowałeś błąd, który został przechwycony, nawet nie rejestrując go w celach debugowania.

 router.get('/:id', function (req, res, next) { database.getData(req.params.userId) .then(function (data) { if (data.length) { res.status(200).json(data); } else { res.status(404).end(); } }) .catch(() => { log.error('db.rest/get: could not get data: ', req.params.userId); res.status(500).json({error: 'Internal server error'}); }) });

Z powyższego widać, że błąd jest całkowicie ignorowany, a kod wysyła 500 do użytkownika, jeśli wywołanie bazy danych nie powiodło się. Ale w rzeczywistości przyczyną awarii bazy danych mogą być zniekształcone dane wysłane przez użytkownika, co jest błędem o kodzie stanu 400.

W powyższym przypadku skończylibyśmy w horrorze debugowania, ponieważ Ty jako programista nie wiedziałbyś, co poszło nie tak. Użytkownik nie będzie w stanie podać przyzwoitego raportu, ponieważ zawsze zgłaszany jest wewnętrzny błąd serwera 500. Skończyłoby się na marnowaniu godzin na znalezienie problemu, co będzie równoznaczne z marnowaniem czasu i pieniędzy pracodawcy.

Przykład ze świata rzeczywistego nr 3: nieakceptowanie błędu rzuconego przez interfejs API

Scenariusz : błąd został zgłoszony z używanego interfejsu API, ale nie akceptujesz tego błędu, zamiast tego organizujesz i przekształcasz błąd w sposób, który czyni go bezużytecznym do celów debugowania.

Weź poniższy przykład kodu:

 async function doThings(input) { try { validate(input); try { await db.create(input); } catch (error) { error.message = `Inner error: ${error.message}` if (error instanceof Klass) { error.isKlass = true; } throw error } } catch (error) { error.message = `Could not do things: ${error.message}`; await rollback(input); throw error; } }

W powyższym kodzie dzieje się wiele, co prowadziłoby do debugowania horroru. Spójrzmy:

  • Zawijanie bloków try/catch : Z powyższego widać, że zawijamy blok try/catch , co jest bardzo złym pomysłem. Zwykle staramy się ograniczyć użycie bloków try/catch , aby zminimalizować powierzchnię, na której musielibyśmy obsłużyć nasz błąd (pomyśl o tym jako o obsłudze błędów DRY);
  • Manipulujemy również komunikatem o błędzie, próbując poprawić, co również nie jest dobrym pomysłem;
  • Sprawdzamy, czy błąd jest instancją typu Klass iw tym przypadku ustawiamy właściwość logiczną błędu isKlass na truev (ale jeśli to sprawdzenie zakończy się pomyślnie, błąd jest typu Klass );
  • Wycofujemy również bazę danych zbyt wcześnie, ponieważ struktura kodu wskazuje na dużą tendencję, że mogliśmy nawet nie trafić do bazy danych, gdy został zgłoszony błąd.

Poniżej jest lepszy sposób na napisanie powyższego kodu:

 async function doThings(input) { validate(input); try { await db.create(input); } catch (error) { try { await rollback(); } catch (error) { logger.log('Rollback failed', error, 'input:', input); } throw error; } }

Przeanalizujmy, co robimy w powyższym fragmencie:

  • Używamy jednego bloku try/catch i tylko w bloku catch używamy innego bloku try/catch , który ma służyć jako strażnik na wypadek, gdyby coś się działo z tą funkcją wycofywania i rejestrujemy to;
  • Na koniec wyrzucamy nasz oryginalny otrzymany błąd, co oznacza, że ​​nie tracimy wiadomości zawartej w tym błędzie.

Testowanie

Najczęściej chcemy przetestować nasz kod (ręcznie lub automatycznie). Ale najczęściej testujemy tylko pozytywne rzeczy. Aby uzyskać solidny test, musisz również przetestować pod kątem błędów i przypadków brzegowych. To zaniedbanie jest odpowiedzialne za to, że błędy trafiają do produkcji, co kosztowałoby więcej czasu na debugowanie.

Wskazówka : zawsze upewnij się, że testujesz nie tylko pozytywne rzeczy (pobieranie kodu stanu 200 z punktu końcowego), ale także wszystkie przypadki błędów i wszystkie przypadki brzegowe.

Przykład ze świata rzeczywistego nr 4: Nieobsłużone odrzucenia

Jeśli wcześniej korzystałeś z obietnic, prawdopodobnie napotkałeś unhandled rejections .

Oto krótki wstęp na temat nieobsługiwanych odrzuceń. Nieobsłużone odrzucenia to odrzucenia obietnic, które nie zostały obsłużone. Oznacza to, że obietnica została odrzucona, ale Twój kod będzie nadal działał.

Spójrzmy na typowy przykład z prawdziwego świata, który prowadzi do nierozpatrzonych odrzuceń..

 'use strict'; async function foobar() { throw new Error('foobar'); } async function baz() { throw new Error('baz') } (async function doThings() { const a = foobar(); const b = baz(); try { await a; await b; } catch (error) { // ignore all errors! } })();

Powyższy kod na pierwszy rzut oka może wydawać się nie podatny na błędy. Ale przy bliższym przyjrzeniu się zaczynamy dostrzegać defekt. Pozwólcie, że wyjaśnię: co się dzieje, gdy a zostaje odrzucone? Oznacza to, że await b nigdy nie zostanie osiągnięte, a to oznacza nieobsłużone odrzucenie. Możliwym rozwiązaniem jest użycie Promise.all na obu obietnicach. Więc kod wyglądałby tak:

 'use strict'; async function foobar() { throw new Error('foobar'); } async function baz() { throw new Error('baz') } (async function doThings() { const a = foobar(); const b = baz(); try { await Promise.all([a, b]); } catch (error) { // ignore all errors! } })();

Oto inny rzeczywisty scenariusz, który doprowadziłby do nieobsłużonego błędu odrzucenia obietnicy:

 'use strict'; async function foobar() { throw new Error('foobar'); } async function doThings() { try { return foobar() } catch { // ignoring errors again ! } } doThings();

Jeśli uruchomisz powyższy fragment kodu, otrzymasz nieobsłużone odrzucenie obietnicy, a oto dlaczego: Chociaż nie jest to oczywiste, zwracamy obietnicę (foobar), zanim zajmiemy się nią za pomocą try/catch . Powinniśmy poczekać na obietnicę, którą obsługujemy za pomocą try/catch , aby kod przeczytał:

 'use strict'; async function foobar() { throw new Error('foobar'); } async function doThings() { try { return await foobar() } catch { // ignoring errors again ! } } doThings();

Podsumowanie negatywnych rzeczy

Teraz, gdy widziałeś nieprawidłowe wzorce obsługi błędów i możliwe poprawki, przejdźmy teraz do wzorca klasy Error i tego, jak rozwiązuje problem nieprawidłowej obsługi błędów w NodeJS.

Klasy błędów

W tym wzorcu uruchomilibyśmy naszą aplikację z klasą ApplicationError w ten sposób, że wiemy, że wszystkie błędy w naszych aplikacjach, które jawnie wyrzucimy, będą z niej dziedziczyć. Zaczęlibyśmy więc od następujących klas błędów:

  • ApplicationError
    Jest to przodek wszystkich innych klas błędów, tj. wszystkie inne klasy błędów dziedziczą po nim.
  • DatabaseError
    Każdy błąd związany z operacjami na bazie danych będzie dziedziczony z tej klasy.
  • UserFacingError
    Każdy błąd powstały w wyniku interakcji użytkownika z aplikacją zostanie odziedziczony z tej klasy.

Oto jak wyglądałby nasz plik klasy error :

 'use strict'; // Here is the base error classes to extend from class ApplicationError extends Error { get name() { return this.constructor.name; } } class DatabaseError extends ApplicationError { } class UserFacingError extends ApplicationError { } module.exports = { ApplicationError, DatabaseError, UserFacingError }

Takie podejście umożliwia nam rozróżnienie błędów zgłaszanych przez naszą aplikację. Więc teraz, jeśli chcemy obsłużyć błąd nieprawidłowego żądania (nieprawidłowe dane wprowadzone przez użytkownika) lub błąd nieznaleziony (nie znaleziono zasobu), możemy dziedziczyć po klasie bazowej, którą jest UserFacingError (jak w kodzie poniżej).

 const { UserFacingError } = require('./baseErrors') class BadRequestError extends UserFacingError { constructor(message, options = {}) { super(message); // You can attach relevant information to the error instance // (eg. the username) for (const [key, value] of Object.entries(options)) { this[key] = value; } } get statusCode() { return 400; } } class NotFoundError extends UserFacingError { constructor(message, options = {}) { super(message); // You can attach relevant information to the error instance // (eg. the username) for (const [key, value] of Object.entries(options)) { this[key] = value; } } get statusCode() { return 404 } } module.exports = { BadRequestError, NotFoundError }

Jedną z korzyści podejścia do klasy error jest to, że jeśli wyrzucimy jeden z tych błędów, na przykład NotFoundError , każdy programista czytający tę bazę kodu będzie w stanie zrozumieć, co się dzieje w tym momencie (jeśli przeczyta kod ).

Byłbyś w stanie przekazać wiele właściwości specyficznych dla każdej klasy błędu, a także podczas tworzenia wystąpienia tego błędu.

Inną kluczową korzyścią jest to, że możesz mieć właściwości, które zawsze są częścią klasy błędu, na przykład, jeśli otrzymasz błąd UserFacing, będziesz wiedział, że statusCode jest zawsze częścią tej klasy błędu, teraz możesz po prostu użyć go bezpośrednio w kod później.

Wskazówki dotyczące korzystania z klas błędów

  • Stwórz swój własny moduł (prawdopodobnie prywatny) dla każdej klasy błędu, w ten sposób możesz po prostu zaimportować go do swojej aplikacji i używać go wszędzie.
  • Zgłaszaj tylko te błędy, na których Ci zależy (błędy będące instancjami Twoich klas błędów). W ten sposób wiesz, że Twoje klasy błędów są jedynym źródłem prawdy i zawiera wszystkie informacje niezbędne do debugowania aplikacji.
  • Posiadanie abstrakcyjnego modułu błędów jest całkiem przydatne, ponieważ teraz wiemy, że wszystkie niezbędne informacje o błędach, które mogą zgłaszać nasze aplikacje, znajdują się w jednym miejscu.
  • Obsługuj błędy w warstwach. Jeśli wszędzie zajmujesz się błędami, masz niespójne podejście do obsługi błędów, co jest trudne do śledzenia. Przez warstwy rozumiem warstwy bazy danych, warstwy express/fastify/HTTP i tak dalej.

Zobaczmy, jak wyglądają klasy błędów w kodzie. Oto przykład ekspresowy:

 const { DatabaseError } = require('./error') const { NotFoundError } = require('./userFacingErrors') const { UserFacingError } = require('./error') // Express app.get('/:id', async function (req, res, next) { let data try { data = await database.getData(req.params.userId) } catch (err) { return next(err); } if (!data.length) { return next(new NotFoundError('Dataset not found')); } res.status(200).json(data) }) app.use(function (err, req, res, next) { if (err instanceof UserFacingError) { res.sendStatus(err.statusCode); // or res.status(err.statusCode).send(err.errorCode) } else { res.sendStatus(500) } // do your logic logger.error(err, 'Parameters: ', req.params, 'User data: ', req.user) });

Z powyższego korzystamy z tego, że Express udostępnia globalną procedurę obsługi błędów, która umożliwia obsługę wszystkich błędów w jednym miejscu. Możesz zobaczyć wywołanie next() w miejscach, w których obsługujemy błędy. To wywołanie przekaże błędy do modułu obsługi zdefiniowanego w sekcji app.use . Ponieważ ekspres nie obsługuje async/await, używamy bloków try/catch .

Tak więc z powyższego kodu, aby obsłużyć nasze błędy wystarczy sprawdzić, czy zgłoszony błąd jest instancją UserFacingError i automatycznie wiemy, że w obiekcie błędu będzie statusCode i wysyłamy go do użytkownika (możesz chcieć mieć również konkretny kod błędu, który można przekazać klientowi) i to prawie wszystko.

Zauważysz również, że w tym wzorcu (wzorzec klasy error ) każdy inny błąd, którego nie zgłosiłeś jawnie, jest błędem 500 , ponieważ jest to coś nieoczekiwanego, co oznacza, że ​​nie zgłosiłeś tego błędu w swojej aplikacji. W ten sposób jesteśmy w stanie rozróżnić rodzaje błędów występujących w naszych aplikacjach.

Wniosek

Właściwa obsługa błędów w aplikacji może sprawić, że będziesz lepiej spać w nocy i skrócić czas debugowania. Oto kilka kluczowych punktów na wynos z tego artykułu:

  • Użyj klas błędów specjalnie skonfigurowanych dla Twojej aplikacji;
  • Implementuj abstrakcyjne programy obsługi błędów;
  • Zawsze używaj async/await;
  • Wyraźne błędy;
  • Użytkownik obiecuje, jeśli to konieczne;
  • Zwróć poprawne statusy i kody błędów;
  • Skorzystaj z haczyków obietnic.

The Smashing Cat odkrywa nowe spostrzeżenia, oczywiście w Smashing Workshops.

Przydatne bity front-end i UX, dostarczane raz w tygodniu.

Z narzędziami, które pomogą Ci lepiej wykonywać swoją pracę. Subskrybuj i otrzymuj listy kontrolne inteligentnego projektowania interfejsu w formacie PDF Witalija za pośrednictwem poczty e-mail.

Na froncie i UX. Zaufany przez 190 000 osób.