Poniżej znajduje się podsumowanie i wybór z wielu różnych źródeł na ten temat, w tym przykład kodu i cytaty z wybranych postów na blogu. Pełna lista najlepszych praktyk znajduje się tutaj
Najlepsze praktyki obsługi błędów Node.JS
Liczba 1: Użyj obietnic do obsługi błędów asynchronicznych
TL; DR: Obsługa błędów asynchronicznych w stylu wywołania zwrotnego jest prawdopodobnie najszybszą drogą do piekła (inaczej piramidy zagłady). Najlepszym prezentem, jaki możesz dać kodowi, jest skorzystanie z renomowanej biblioteki obietnic, która zapewnia bardzo zwartą i znaną składnię kodu, taką jak try-catch
W przeciwnym razie: styl wywołania zwrotnego Node.JS, funkcja (err, odpowiedź), jest obiecującym sposobem na niemożliwy do utrzymania kod ze względu na połączenie obsługi błędów z przypadkowym kodem, nadmiernym zagnieżdżaniem i niewygodnymi wzorcami kodowania
Przykład kodu - dobrze
doWork()
.then(doWork)
.then(doError)
.then(doWork)
.catch(errorHandler)
.then(verify);
przykład kodu anty wzorzec - obsługa błędów w stylu wywołania zwrotnego
getData(someParameter, function(err, result){
if(err != null)
//do something like calling the given callback function and pass the error
getMoreData(a, function(err, result){
if(err != null)
//do something like calling the given callback function and pass the error
getMoreData(b, function(c){
getMoreData(d, function(e){
...
});
});
});
});
});
Cytat na blogu: „Mamy problem z obietnicami”
(z pouchdb na blogu, w rankingu 11 słów kluczowych „Obietnice węzłowe”)
„… W rzeczywistości wywołania zwrotne robią coś jeszcze bardziej złowieszczego: pozbawiają nas stosu, co zwykle jest czymś oczywistym w językach programowania. Pisanie kodu bez stosu przypomina jazdę samochodem bez pedału hamulca: ty nie zdawaj sobie sprawy z tego, jak bardzo go potrzebujesz, dopóki go nie sięgniesz, a go nie ma. Cała obietnica polega na zwróceniu podstaw języka, które straciliśmy, kiedy osiągnęliśmy asynchronię: powrót, rzut i stos. Ale ty muszą wiedzieć, jak prawidłowo korzystać z obietnic, aby z nich skorzystać ”.
Number2: Używaj tylko wbudowanego obiektu Error
TL; DR: Kod, który generuje błędy jako ciąg znaków lub niestandardowy typ, jest dość powszechny - komplikuje to logikę obsługi błędów i interoperacyjność między modułami. Niezależnie od tego, czy odrzucisz obietnicę, wyrzucisz wyjątek, czy wyemitujesz błąd - użycie wbudowanego obiektu Error Node.JS zwiększa jednolitość i zapobiega utracie informacji o błędzie
W przeciwnym razie: podczas wykonywania jakiegoś modułu brak pewności, jaki rodzaj błędów w zamian wraca - znacznie utrudnia rozumowanie nadchodzącego wyjątku i obsługiwanie go. Warto nawet użyć niestandardowych typów do opisania błędów, które mogą prowadzić do utraty krytycznych informacji o błędach, takich jak ślad stosu!
Przykład kodu - robienie tego dobrze
//throwing an Error from typical function, whether sync or async
if(!productToAdd)
throw new Error("How can I add new product when no value provided?");
//'throwing' an Error from EventEmitter
const myEmitter = new MyEmitter();
myEmitter.emit('error', new Error('whoops!'));
//'throwing' an Error from a Promise
return new promise(function (resolve, reject) {
DAL.getProduct(productToAdd.id).then((existingProduct) =>{
if(existingProduct != null)
return reject(new Error("Why fooling us and trying to add an existing product?"));
przykładowy kod anty wzorca
//throwing a String lacks any stack trace information and other important properties
if(!productToAdd)
throw ("How can I add new product when no value provided?");
Cytat na blogu: „Ciąg nie jest błędem”
(z blogu, który zajął 6 miejsce w przypadku słów kluczowych „Obiekt błędu Node.JS”)
„… Przekazanie ciągu zamiast błędu powoduje zmniejszenie interoperacyjności między modułami. Łamie kontrakty z interfejsami API, które mogą wykonywać instancje kontroli błędów lub chcą dowiedzieć się więcej o błędzie . Obiekty błędów, jak zobaczymy, mają bardzo ciekawe właściwości we współczesnych silnikach JavaScript oprócz trzymania wiadomości przekazywanej konstruktorowi. "
Liczba 3: Rozróżnij błędy operacyjne i programistyczne
TL; DR: Błędy operacyjne (np. API otrzymało niepoprawne dane wejściowe) odnoszą się do znanych przypadków, w których wpływ błędu jest w pełni zrozumiały i można go starannie rozpatrzyć. Z drugiej strony błąd programisty (np. Próba odczytania niezdefiniowanej zmiennej) odnosi się do nieznanych błędów kodu, które zmuszają do płynnego restartu aplikacji
W przeciwnym razie: zawsze możesz ponownie uruchomić aplikację, gdy pojawi się błąd, ale po co zawieść ~ 5000 użytkowników online z powodu drobnego i przewidywanego błędu (błędu operacyjnego)? wręcz przeciwnie, nie jest też idealne - utrzymanie aplikacji w stanie, gdy wystąpi nieznany problem (błąd programisty), może spowodować nieprzewidziane zachowanie. Rozróżnienie tych dwóch pozwala działać taktownie i stosować zrównoważone podejście oparte na danym kontekście
Przykład kodu - robienie tego dobrze
//throwing an Error from typical function, whether sync or async
if(!productToAdd)
throw new Error("How can I add new product when no value provided?");
//'throwing' an Error from EventEmitter
const myEmitter = new MyEmitter();
myEmitter.emit('error', new Error('whoops!'));
//'throwing' an Error from a Promise
return new promise(function (resolve, reject) {
DAL.getProduct(productToAdd.id).then((existingProduct) =>{
if(existingProduct != null)
return reject(new Error("Why fooling us and trying to add an existing product?"));
przykład kodu - oznaczenie błędu jako działającego (zaufanego)
//marking an error object as operational
var myError = new Error("How can I add new product when no value provided?");
myError.isOperational = true;
//or if you're using some centralized error factory (see other examples at the bullet "Use only the built-in Error object")
function appError(commonType, description, isOperational) {
Error.call(this);
Error.captureStackTrace(this);
this.commonType = commonType;
this.description = description;
this.isOperational = isOperational;
};
throw new appError(errorManagement.commonErrors.InvalidInput, "Describe here what happened", true);
//error handling code within middleware
process.on('uncaughtException', function(error) {
if(!error.isOperational)
process.exit(1);
});
Cytat na blogu : „W przeciwnym razie ryzykujesz stanem” (z bloga do debugowania, w rankingu 3 dla słów kluczowych „Nachwytany wyjątek Node.JS”)
„ … Ze względu na charakter działania rzucania w JavaScript, prawie nigdy nie ma sposobu, aby bezpiecznie„ odebrać od miejsca, w którym przerwałeś ”, bez wycieków referencji lub stworzenia innego rodzaju niezdefiniowanego stanu kruchości. Najbezpieczniejszy sposób na wyrzuconym błędem jest zamknięcie procesu . Oczywiście na normalnym serwerze internetowym może być otwartych wiele połączeń i nie jest rozsądne gwałtowne zamykanie tych połączeń, ponieważ błąd został wywołany przez kogoś innego. Lepszym rozwiązaniem jest wyślij odpowiedź o błędzie na żądanie, które spowodowało błąd, jednocześnie pozwalając innym zakończyć pracę w normalnym czasie i przestań nasłuchiwać nowych żądań w tym procesie roboczym ”
Number4: Obsługuj błędy centralnie, ale nie w oprogramowaniu pośrednim
TL; DR: Logika obsługi błędów, taka jak poczta do administratora i rejestrowanie, powinna być zamknięta w dedykowanym i scentralizowanym obiekcie, do którego wywołują wszystkie punkty końcowe (np. Express middleware, zadania cron, testowanie jednostek), gdy wystąpi błąd.
W przeciwnym razie: Brak obsługi błędów w jednym miejscu doprowadzi do duplikacji kodu i prawdopodobnie do błędów, które są obsługiwane nieprawidłowo
Przykład kodu - typowy przepływ błędów
//DAL layer, we don't handle errors here
DB.addDocument(newCustomer, (error, result) => {
if (error)
throw new Error("Great error explanation comes here", other useful parameters)
});
//API route code, we catch both sync and async errors and forward to the middleware
try {
customerService.addNew(req.body).then(function (result) {
res.status(200).json(result);
}).catch((error) => {
next(error)
});
}
catch (error) {
next(error);
}
//Error handling middleware, we delegate the handling to the centrzlied error handler
app.use(function (err, req, res, next) {
errorHandler.handleError(err).then((isOperationalError) => {
if (!isOperationalError)
next(err);
});
});
Cytat na blogu: „Czasami niższe poziomy nie mogą zrobić nic użytecznego oprócz propagowania błędu do osoby dzwoniącej” (z bloga Joyent, w rankingu 1 dla słów kluczowych „Obsługa błędów Node.JS”)
„… Może się to wiązać z obsługą tego samego błędu na kilku poziomach stosu. Dzieje się tak, gdy niższe poziomy nie mogą zrobić nic pożytecznego oprócz propagowania błędu do swojego wywołującego, który propaguje błąd do swojego wywołującego itd. Często tylko osoba dzwoniąca najwyższego poziomu wie, jaka jest odpowiednia odpowiedź, czy to jest ponowienie operacji, zgłoszenie błędu użytkownikowi, czy coś innego. Ale to nie znaczy, że powinieneś spróbować zgłosić wszystkie błędy do jednego najwyższego poziomu wywołanie zwrotne, ponieważ samo to wywołanie zwrotne nie może wiedzieć, w jakim kontekście wystąpił błąd „
Number5: Błędy interfejsu API dokumentu za pomocą Swagger
TL; DR: Poinformuj dzwoniących API, które błędy mogą w zamian wrócić, aby mogli sobie z nimi dobrze poradzić bez awarii. Zwykle odbywa się to za pomocą ram dokumentacji API REST, takich jak Swagger
W przeciwnym razie: klient API może zdecydować o awarii i ponownym uruchomieniu tylko dlatego, że otrzymał błąd, którego nie mógł zrozumieć. Uwaga: osobą wywołującą interfejs API może być Ty (bardzo typowe w środowisku mikrousług)
Cytat na blogu: „Musisz powiedzieć swoim rozmówcom, jakie błędy mogą się zdarzyć” (z bloga Joyent, w rankingu 1 dla słów kluczowych „Logowanie do Node.JS”)
… Rozmawialiśmy o tym, jak radzić sobie z błędami, ale kiedy piszesz nową funkcję, w jaki sposób dostarczasz błędy do kodu, który wywołał twoją funkcję? … Jeśli nie wiesz, jakie błędy mogą się zdarzyć, lub nie wiesz, co one oznaczają, oznacza to, że Twój program nie może być poprawny inaczej niż przez przypadek. Więc jeśli piszesz nową funkcję, musisz powiedzieć swoim rozmówcom, jakie błędy mogą się zdarzyć i co one oznaczają
Numer 6: Zamknij proces z wdziękiem, gdy do miasta przybywa nieznajomy
TL; DR: Gdy wystąpi nieznany błąd (błąd programisty, patrz najlepsza praktyka nr 3) - nie ma pewności co do kondycji aplikacji. Powszechna praktyka sugeruje ostrożne ponowne uruchomienie procesu przy użyciu narzędzia „restartowania”, takiego jak Forever i PM2
W przeciwnym razie: gdy zostanie wykryty nieznany wyjątek, jakiś obiekt może być w złym stanie (np. Emiter zdarzeń, który jest używany globalnie i nie uruchamia już zdarzeń z powodu wewnętrznej awarii), a wszystkie przyszłe żądania mogą zawieść lub zachowywać się szaleńczo
Przykład kodu - decydowanie o awarii
//deciding whether to crash when an uncaught exception arrives
//Assuming developers mark known operational errors with error.isOperational=true, read best practice #3
process.on('uncaughtException', function(error) {
errorManagement.handler.handleError(error);
if(!errorManagement.handler.isTrustedError(error))
process.exit(1)
});
//centralized error handler encapsulates error-handling related logic
function errorHandler(){
this.handleError = function (error) {
return logger.logError(err).then(sendMailToAdminIfCritical).then(saveInOpsQueueIfCritical).then(determineIfOperationalError);
}
this.isTrustedError = function(error)
{
return error.isOperational;
}
Cytat z bloga: „Istnieją trzy szkoły myślenia na temat obsługi błędów” (z bloga jsrecipes)
… Istnieją przede wszystkim trzy szkoły myślenia na temat obsługi błędów: 1. Pozwól aplikacji ulec awarii i uruchom ją ponownie. 2. Obsługuj wszystkie możliwe błędy i nigdy nie zawieszaj się. 3. Zrównoważone podejście między nimi
Number7: Użyj dojrzałego programu rejestrującego, aby zwiększyć widoczność błędów
TL; DR: Zestaw dojrzałych narzędzi do rejestrowania, takich jak Winston, Bunyan lub Log4J, przyspieszy wykrywanie błędów i zrozumienie. Więc zapomnij o console.log.
W przeciwnym razie: przeglądanie przez plik console.logs lub ręcznie przez niechlujny plik tekstowy bez korzystania z narzędzi zapytań lub porządnej przeglądarki dziennika może być zajęty w pracy do późna
Przykład kodu - rejestrator Winston w akcji
//your centralized logger object
var logger = new winston.Logger({
level: 'info',
transports: [
new (winston.transports.Console)(),
new (winston.transports.File)({ filename: 'somefile.log' })
]
});
//custom code somewhere using the logger
logger.log('info', 'Test Log Message with some parameter %s', 'some parameter', { anything: 'This is metadata' });
Cytat na blogu: „Pozwala zidentyfikować kilka wymagań (dla loggera):” (z blogu strongblog)
… Pozwala zidentyfikować kilka wymagań (dla rejestratora): 1. Znacznik czasu każdej linii dziennika. Ten jest dość oczywisty - powinieneś być w stanie powiedzieć, kiedy pojawił się każdy wpis w dzienniku. 2. Format rejestrowania powinien być łatwo przyswajalny zarówno przez ludzi, jak i maszyny. 3. Pozwala na wiele konfigurowalnych strumieni docelowych. Na przykład możesz zapisywać dzienniki śledzenia w jednym pliku, ale gdy napotkasz błąd, napisz do tego samego pliku, a następnie do pliku błędu i wyślij wiadomość e-mail w tym samym czasie…
Number8: Odkryj błędy i przestoje przy użyciu produktów APM
TL; DR: Produkty do monitorowania i wydajności (inaczej APM) proaktywnie oceniają twoją bazę kodu lub API, aby mogły automatycznie magicznie wyróżniać błędy, awarie i spowalniające brakujące części
W przeciwnym razie: możesz poświęcić wiele wysiłku na pomiar wydajności interfejsu API i przestojów, prawdopodobnie nigdy nie będziesz wiedział, jakie są twoje najwolniejsze części kodu w rzeczywistym świecie i jak wpływają one na UX
Cytat z bloga: „Segmenty produktów APM” (z bloga Yoni Goldberg)
„… Produkty APM stanowią 3 główne segmenty: 1. Monitorowanie strony internetowej lub interfejsu API - usługi zewnętrzne, które stale monitorują czas działania i wydajność za pośrednictwem żądań HTTP. Można je skonfigurować w kilka minut. Oto kilka wybranych konkurentów: Pingdom, Uptime Robot i New Relic
2 Oprzyrządowanie kodu - rodzina produktów, które wymagają osadzenia agenta w aplikacji, aby skorzystać z powolnego wykrywania kodu, statystyk wyjątków, monitorowania wydajności i wielu innych. Oto kilka wybranych kandydatów: Nowa relikwia, dynamika aplikacji
3. Pulpit nawigacyjny wywiadu operacyjnego -Ta linia produktów koncentruje się na ułatwianiu zespołowi operacyjnemu pomiarów i dobranych treści, które pozwalają łatwo utrzymać najwyższą wydajność aplikacji. Zwykle wymaga to agregacji wielu źródeł informacji (dzienników aplikacji, dzienników DB, dzienników serwerów itp.) I wstępnych prac projektowych na desce rozdzielczej. Oto kilka wybranych kandydatów: Datadog, Splunk ”
Powyżej jest skróconą wersją - zobacz tutaj więcej najlepszych praktyk i przykładów