Obsługa wyjątków od najlepszych praktyk Node.js

755

Właśnie zacząłem wypróbowywać node.js kilka dni temu. Uświadomiłem sobie, że Węzeł jest przerywany, ilekroć mam nieobsługiwany wyjątek w moim programie. Różni się to od zwykłego kontenera serwera, na który byłem narażony, gdzie umiera tylko wątek roboczy, gdy wystąpią nieobsługiwane wyjątki, a kontener nadal będzie mógł otrzymać żądanie. Rodzi to kilka pytań:

  • Czy process.on('uncaughtException')to jedyny skuteczny sposób, aby się przed tym uchronić?
  • Będzie process.on('uncaughtException')złapać nieobsługiwany wyjątek podczas wykonywania procesów asynchronicznych, jak również?
  • Czy istnieje już moduł (np. Wysyłanie wiadomości e-mail lub zapis do pliku), który można wykorzystać w przypadku nieprzechwyconych wyjątków?

Byłbym wdzięczny za każdy wskaźnik / artykuł, który pokazywałby mi wspólne najlepsze praktyki postępowania z nieprzechwyconymi wyjątkami w node.js

momo
źródło
11
nieprzechwycone wyjątki nie powinny się zdarzyć. Jeśli robią użyciu program, który ponownie uruchomi całą aplikację, gdy jej upaść (nodemon, zawsze, przełożony)
Raynos
116
Nieprzechwycone wyjątki zawsze mogą się zdarzyć, chyba że umieścisz w środku każdy fragment kodu asynchronicznego try .. catchi sprawdzisz, czy dzieje się tak również w przypadku wszystkich bibliotek
Dan
13
+1 Dan Na początku myślałem, że wszystkie twoje biblioteki to trochę przesada, ponieważ „tylko” musisz owinąć wszystkie „punkty wejścia wątku” w kodzie w try / catches. Ale o tym myśleć bardziej dokładnie, każdy lib mogłyby mieć setTimeoutlub setIntervalczy coś w tym rodzaju zakopane gdzieś głęboko, które nie mogą być objęte kodem.
Eugene Beresovsky
8
@EugeneBeresovksy Dan ma rację, ale nie zmienia to faktu, że gdy pojawią się nieprzechwycone wyjątki, jedyną bezpieczną opcją jest ponowne uruchomienie aplikacji. Innymi słowy, Twoja aplikacja uległa awarii i nie możesz nic zrobić ani zrobić. Jeśli chcesz zrobić coś konstruktywnego, zaimplementuj nową, wciąż eksperymentalną funkcję domeny v0.8, aby można było zarejestrować awarię i wysłać odpowiedź 5xx do klienta.
ostergaard
1
@ Dan Nawet załączenie wszystkich funkcji zwrotnych w try .. catch nie gwarantuje błędów wychwytywania. W przypadku, gdy wymagany moduł korzysta z własnych plików binarnych, mogą one zawieść bezkrytycznie. Zdarzyło mi się to z phantomjs-node, który zawodzi w przypadku błędów, których nie można wyłapać (chyba że miałbym przeprowadzić jakąś kontrolę procesu na wymaganych plikach binarnych, ale nigdy tego nie śledziłem).
Trindaz

Odpowiedzi:

737

Aktualizacja: Joyent ma teraz swój własny przewodnik . Poniższe informacje są bardziej podsumowaniem:

Błędy bezpiecznego „rzucania”

Idealnie chcielibyśmy uniknąć nieprzechwyconych błędów tak bardzo, jak to możliwe, zamiast dosłownie zgłaszać błąd, zamiast tego możemy bezpiecznie „wyrzucić” błąd, korzystając z jednej z następujących metod, w zależności od naszej architektury kodu:

  • W przypadku kodu synchronicznego, jeśli wystąpi błąd, zwróć błąd:

    // Define divider as a syncrhonous function
    var divideSync = function(x,y) {
        // if error condition?
        if ( y === 0 ) {
            // "throw" the error safely by returning it
            return new Error("Can't divide by zero")
        }
        else {
            // no error occured, continue on
            return x/y
        }
    }
    
    // Divide 4/2
    var result = divideSync(4,2)
    // did an error occur?
    if ( result instanceof Error ) {
        // handle the error safely
        console.log('4/2=err', result)
    }
    else {
        // no error occured, continue on
        console.log('4/2='+result)
    }
    
    // Divide 4/0
    result = divideSync(4,0)
    // did an error occur?
    if ( result instanceof Error ) {
        // handle the error safely
        console.log('4/0=err', result)
    }
    else {
        // no error occured, continue on
        console.log('4/0='+result)
    }
  • W przypadku kodu opartego na wywołaniu zwrotnym (tj. Asynchronicznym) pierwszym argumentem wywołania zwrotnego jest to err, że jeśli wystąpi błąd, errto błąd, jeśli błąd się nie pojawi, to errjest null. Wszelkie inne argumenty następują po errargumencie:

    var divide = function(x,y,next) {
        // if error condition?
        if ( y === 0 ) {
            // "throw" the error safely by calling the completion callback
            // with the first argument being the error
            next(new Error("Can't divide by zero"))
        }
        else {
            // no error occured, continue on
            next(null, x/y)
        }
    }
    
    divide(4,2,function(err,result){
        // did an error occur?
        if ( err ) {
            // handle the error safely
            console.log('4/2=err', err)
        }
        else {
            // no error occured, continue on
            console.log('4/2='+result)
        }
    })
    
    divide(4,0,function(err,result){
        // did an error occur?
        if ( err ) {
            // handle the error safely
            console.log('4/0=err', err)
        }
        else {
            // no error occured, continue on
            console.log('4/0='+result)
        }
    })
  • Dla pełnego wrażeń kodu, gdzie błąd może zdarzyć się wszędzie, zamiast rzucać się błąd, ogień errorzdarzenie zamiast :

    // Definite our Divider Event Emitter
    var events = require('events')
    var Divider = function(){
        events.EventEmitter.call(this)
    }
    require('util').inherits(Divider, events.EventEmitter)
    
    // Add the divide function
    Divider.prototype.divide = function(x,y){
        // if error condition?
        if ( y === 0 ) {
            // "throw" the error safely by emitting it
            var err = new Error("Can't divide by zero")
            this.emit('error', err)
        }
        else {
            // no error occured, continue on
            this.emit('divided', x, y, x/y)
        }
    
        // Chain
        return this;
    }
    
    // Create our divider and listen for errors
    var divider = new Divider()
    divider.on('error', function(err){
        // handle the error safely
        console.log(err)
    })
    divider.on('divided', function(x,y,result){
        console.log(x+'/'+y+'='+result)
    })
    
    // Divide
    divider.divide(4,2).divide(4,0)

Bezpiecznie „łapanie” błędów

Czasami jednak może nadal istnieć kod, który generuje błąd, który może prowadzić do nieprzechwyconego wyjątku i potencjalnej awarii naszej aplikacji, jeśli nie złapiemy go bezpiecznie. W zależności od naszej architektury kodu możemy użyć jednej z następujących metod, aby ją złapać:

  • Kiedy wiemy, gdzie występuje błąd, możemy owinąć tę sekcję w domenie node.js

    var d = require('domain').create()
    d.on('error', function(err){
        // handle the error safely
        console.log(err)
    })
    
    // catch the uncaught errors in this asynchronous or synchronous code block
    d.run(function(){
        // the asynchronous or synchronous code that we want to catch thrown errors on
        var err = new Error('example')
        throw err
    })
  • Jeśli wiemy, gdzie występuje błąd, to kod synchroniczny i z jakiegokolwiek powodu nie można używać domen (być może starej wersji węzła), możemy użyć instrukcji try catch:

    // catch the uncaught errors in this synchronous code block
    // try catch statements only work on synchronous code
    try {
        // the synchronous code that we want to catch thrown errors on
        var err = new Error('example')
        throw err
    } catch (err) {
        // handle the error safely
        console.log(err)
    }

    Uważaj jednak, aby nie używać try...catchw kodzie asynchronicznym, ponieważ nie zostanie wyłapany błąd asynchroniczny:

    try {
        setTimeout(function(){
            var err = new Error('example')
            throw err
        }, 1000)
    }
    catch (err) {
        // Example error won't be caught here... crashing our app
        // hence the need for domains
    }

    Jeśli chcesz pracować try..catchw połączeniu z kodem asynchronicznym, podczas uruchamiania Węzła 7.4 lub nowszego możesz używać async/awaitnatywnie do pisania funkcji asynchronicznych.

    Kolejną rzeczą, na którą należy uważać, try...catchjest ryzyko zawinięcia wywołania zwrotnego zakończenia w tryinstrukcji:

    var divide = function(x,y,next) {
        // if error condition?
        if ( y === 0 ) {
            // "throw" the error safely by calling the completion callback
            // with the first argument being the error
            next(new Error("Can't divide by zero"))
        }
        else {
            // no error occured, continue on
            next(null, x/y)
        }
    }
    
    var continueElsewhere = function(err, result){
            throw new Error('elsewhere has failed')
    }
    
    try {
            divide(4, 2, continueElsewhere)
            // ^ the execution of divide, and the execution of 
            //   continueElsewhere will be inside the try statement
    }
    catch (err) {
            console.log(err.stack)
            // ^ will output the "unexpected" result of: elsewhere has failed
    }

    Gotcha jest bardzo łatwa, ponieważ kod staje się bardziej złożony. W związku z tym najlepiej jest albo użyć domen, albo zwrócić błędy, aby uniknąć (1) nieprzechwyconych wyjątków w kodzie asynchronicznym (2) próbowania przechwytywania wykonania, którego nie chcesz. W językach, które pozwalają na właściwe wątkowanie zamiast asynchronicznego stylu JavaScript w maszynie zdarzeń, nie stanowi to większego problemu.

  • Wreszcie w przypadku, gdy nieprzechwycony błąd wystąpi w miejscu, które nie zostało opakowane w domenie lub w instrukcji try catch, możemy sprawić, że nasza aplikacja nie ulegnie awarii przy użyciu uncaughtExceptionnasłuchiwania (jednak może to spowodować, że aplikacja będzie w nieznanym stanie ):

    // catch the uncaught errors that weren't wrapped in a domain or try catch statement
    // do not use this in modules, but only in applications, as otherwise we could have multiple of these bound
    process.on('uncaughtException', function(err) {
        // handle the error safely
        console.log(err)
    })
    
    // the asynchronous or synchronous code that emits the otherwise uncaught error
    var err = new Error('example')
    throw err
balupton
źródło
5
Dzięki, Raynos, zaktualizowano. Czy masz źródło, które wyjaśnia zło try catch? Chciałbym poprzeć to dowodami. Naprawiono także przykład synchronizacji.
balupton,
2
Ta odpowiedź nie jest już ważna. Domeny rozwiązują ten problem (zalecane przez node.js)
Gabriel Llamas
5
@balupton Błędy powinny być zgłaszane do obsługi błędów. Zdecydowanie NIE należy ich unikać. Nie ma w nich nic, co mogłoby zakłócić działanie aplikacji lub czegokolwiek innego. Java i większość innych współczesnych języków ma doskonałą obsługę wyjątków. Mój jedyny wniosek po przeczytaniu niektórych źle sformułowanych postów jest taki, że ludzie nie rozumieją ich zbyt dobrze, a więc się ich boją. Strach Niepewne wątpliwości. Ta debata została rozstrzygnięta na korzyść wyjątków co najmniej 20 lat temu.
powiększenie,
22
Teraz domeny są przestarzałe przez io.js : „ Ten moduł oczekuje na wycofanie. Po sfinalizowaniu zastępczego interfejsu API, ten moduł będzie całkowicie przestarzały… Użytkownicy, którzy absolutnie muszą mieć funkcje zapewniane przez domeny, mogą na nim polegać, ale w przyszłości należy spodziewać się migracji do innego rozwiązania ”.
Timothy Gu
5
Interfejs API domeny jest już przestarzały ? Wspominają o zamiennym interfejsie API - ktoś wie, kiedy to wyjdzie i jak będzie wyglądać?
UpTheCreek,
95

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

Yonatan
źródło
30

Możesz wyłapać niewyłapane wyjątki, ale ma on ograniczone zastosowanie. Zobacz http://debuggable.com/posts/node-js-dealing-with-uncaught-exceptions:4c933d54-1428-443c-928d-4e1ecbdd56cb

monit, foreverLub upstartmogą być wykorzystane do ponownego uruchomienia procesu węzła kiedy się zawiesi. Pełne wdzięku zamknięcie jest najlepszym, na co możesz liczyć (np. Zapisz wszystkie dane w pamięci w nieprzechwyconym module obsługi wyjątków).

nponeccop
źródło
4
+1 Link jest przydatny, dzięki. Wciąż szukam najlepszych praktyk i znaczenia „płynnego restartu” w kontekście node.js
momo
Moje rozumienie „wdzięcznego restartu” w tym kontekście byłoby zasadniczo tym, co sugeruje nponeccop: pozwól procesowi umrzeć, a wszystko, co go uruchamia, zrestartuj go.
Ilkka
Wielkie dzięki za ten link! Naprawdę użyteczne!
SatheeshJM
To świetna odpowiedź. Jednak nie zgadzam się na zwrócenie błędu w pierwszym przykładzie. ErrorZwrócenie an powoduje, że zwracana wartość jest polimorficzna, co niepotrzebnie zamazuje semantykę funkcji. Ponadto, nurkowanie przez 0 jest już obsługiwane w JavaScript dając Infinity, -Infinitylub NaN, tam gdzie wartości typeof === 'number'. Można je sprawdzić za pomocą !isFinite(value). Ogólnie polecam nigdy nie zwracać błędu z funkcji. Lepsze pod względem czytelności kodu i konserwacji, aby rzucić lub zwrócić specjalną niepolimorficzną wartość w / spójną semantykę.
wprl
13

Domeny nodejs to najnowocześniejszy sposób obsługi błędów w nodejs. Domeny mogą rejestrować zarówno błędy / inne zdarzenia, jak i obiekty tradycyjnie wyrzucane. Domeny zapewniają również funkcjonalność do obsługi wywołań zwrotnych z błędem przekazanym jako pierwszy argument za pomocą metody przechwytywania.

Podobnie jak w przypadku normalnej obsługi błędów typu try / catch, najlepiej jest wyrzucać błędy, gdy wystąpią, i blokować obszary, w których chcesz odizolować błędy od wpływu na resztę kodu. Sposobem na „zablokowanie” tych obszarów jest wywołanie domain.run z funkcją jako blok izolowanego kodu.

W kodzie synchronicznym wystarczy powyższe - gdy wystąpi błąd, albo przepuszczasz go, albo łapiesz i obsługujesz, przywracając dane, które chcesz przywrócić.

try {  
  //something
} catch(e) {
  // handle data reversion
  // probably log too
}

Gdy błąd występuje w asynchronicznym wywołaniu zwrotnym, musisz być w stanie w pełni obsłużyć wycofywanie danych (stan współużytkowany, dane zewnętrzne, takie jak bazy danych itp.). LUB musisz ustawić coś, co wskaże, że wystąpił wyjątek - gdziekolwiek zależy ci na tej fladze, musisz poczekać na zakończenie wywołania zwrotnego.

var err = null;
var d = require('domain').create();
d.on('error', function(e) {
  err = e;
  // any additional error handling
}
d.run(function() { Fiber(function() {
  // do stuff
  var future = somethingAsynchronous();
  // more stuff

  future.wait(); // here we care about the error
  if(err != null) {
    // handle data reversion
    // probably log too
  }

})});

Część powyższego kodu jest brzydka, ale możesz stworzyć dla siebie wzory, aby uczynić go ładniejszym, np .:

var specialDomain = specialDomain(function() {
  // do stuff
  var future = somethingAsynchronous();
  // more stuff

  future.wait(); // here we care about the error
  if(specialDomain.error()) {
    // handle data reversion
    // probably log too
  } 
}, function() { // "catch"
  // any additional error handling
});

AKTUALIZACJA (2013-09):

Powyżej używam przyszłości, która implikuje semantykę włókien , która pozwala czekać na futures w linii. To pozwala na użycie tradycyjnych bloków try-catch do wszystkiego - co uważam za najlepszy sposób. Jednak nie zawsze możesz to zrobić (np. W przeglądarce) ...

Istnieją również kontrakty terminowe, które nie wymagają semantyki włókien (które następnie działają z normalnym JavaScript do przeglądania). Można je nazwać futures, obietnicami lub odroczeniami (odtąd będę odnosił się do futures). Zwykłe biblioteki skryptów JavaScript zezwalają na propagowanie błędów między futures. Tylko niektóre z tych bibliotek pozwalają na prawidłową obsługę każdej rzuconej przyszłości, więc uważaj.

Przykład:

returnsAFuture().then(function() {
  console.log('1')
  return doSomething() // also returns a future

}).then(function() {
  console.log('2')
  throw Error("oops an error was thrown")

}).then(function() {
  console.log('3')

}).catch(function(exception) {
  console.log('handler')
  // handle the exception
}).done()

To naśladuje normalny try-catch, mimo że elementy są asynchroniczne. Wypisałby:

1
2
handler

Zauważ, że nie wypisuje „3”, ponieważ został zgłoszony wyjątek, który przerywa ten przepływ.

Spójrz na obietnice bluebird:

Zauważ, że nie znalazłem wielu innych bibliotek oprócz tych, które poprawnie obsługują zgłoszone wyjątki. Na przykład jQuery jest odroczony - nie - moduł obsługi „fail” nigdy nie zgłasza wyjątku dla procedury obsługi „wtedy”, która moim zdaniem stanowi przełom.

BT
źródło
Właściwa specyfikacja obietnic w Javascript jest znana jako Promises / A +. Możesz zobaczyć listę wdrożeń tutaj: github.com/promises-aplus/promises-spec/blob/master/… . Zauważ, że gołe Obietnice / A + są w praktyce bezużyteczne - Obietnice / A + wciąż pozostawiają wiele praktycznych problemów dla bibliotek do samodzielnego rozwiązania. Gwarantujemy jednak absolutnie niezbędne rzeczy, takie jak propagacja błędu, którą pokazujesz, deterministyczna kolejność wykonywania oraz bezpieczeństwo przed przepełnieniem stosu.
Esailija,
11

Pisałem o tym niedawno na http://snmaynard.com/2012/12/21/node-error-handling/ . Nową funkcją węzła w wersji 0.8 są domeny i pozwalają łączyć wszystkie formy obsługi błędów w jedną łatwiejszą formę zarządzania. Możesz przeczytać o nich w moim poście.

Możesz także użyć czegoś takiego jak Bugsnag, aby śledzić swoje nieprzechwycone wyjątki i otrzymywać powiadomienia przez e-mail, czat lub utworzyć bilet dla nieprzechwyconego wyjątku (jestem współzałożycielem Bugsnag).

Simon Maynard
źródło
2
Moduł domeny jest teraz oficjalnie przestarzały. nodejs.org/api/domain.html
MattSidor
3

Chciałbym tylko dodać, że biblioteka Step.js pomaga radzić sobie z wyjątkami, zawsze przekazując ją do funkcji następnego kroku. Dlatego możesz mieć jako ostatni krok funkcję sprawdzającą pod kątem błędów w dowolnym z poprzednich kroków. Takie podejście może znacznie uprościć obsługę błędów.

Poniżej cytat ze strony github:

wszelkie zgłoszone wyjątki są wychwytywane i przekazywane jako pierwszy argument do następnej funkcji. Tak długo, jak nie zagnieżdżasz funkcji zwrotnych w swoich głównych funkcjach, zapobiegnie to występowaniu wyjątków. Jest to bardzo ważne dla długo działających serwerów node.JS, ponieważ pojedynczy nieprzechwycony wyjątek może doprowadzić do awarii całego serwera.

Co więcej, możesz użyć Step do kontrolowania wykonywania skryptów, aby sekcja czyszczenia była ostatnim krokiem. Na przykład, jeśli chcesz napisać skrypt kompilacji w węźle i zgłosić, ile czasu zajęło mu napisanie, możesz to zrobić w ostatnim kroku (zamiast próbować wykopać ostatnie wywołanie zwrotne).

Michael Yagudaev
źródło
3

Jednym z przypadków, w których użycie try-catch może być odpowiednie, jest użycie pętli forEach. Jest synchroniczny, ale jednocześnie nie można po prostu użyć instrukcji return w wewnętrznym zakresie. Zamiast tego można zastosować metodę try-catch, aby zwrócić obiekt Error w odpowiednim zakresie. Rozważać:

function processArray() {
    try { 
       [1, 2, 3].forEach(function() { throw new Error('exception'); }); 
    } catch (e) { 
       return e; 
    }
}

Jest to kombinacja podejść opisanych powyżej przez @balupton.

Michael Yagudaev
źródło
Zamiast zgłaszania błędów, niektórzy programiści zalecają użycie koncepcji Wynik z Rust, aby zwrócić OK lub Błąd , gdy awaria jest znaną możliwością. Dzięki temu awarie są oddzielone od nieoczekiwanych błędów. Jedną z implementacji tego JS jest wynik r .
joeytwiddle
To decyzja projektowa obejmująca całą aplikację. Myślę, że twoja koncepcja zwracania błędów jest w przybliżeniu równoważna i łatwa do rozpoczęcia (bez dodatkowych zależności), ale mniej wyraźna ( Wynik sprawia, że ​​boleśnie zdajesz sobie sprawę, kiedy awarie mogą wymagać naprawy) i mniej wydajna w tych przypadkach, gdy stos jest zbudowany niepotrzebnie.
joeytwiddle
1

Po przeczytaniu tego postu jakiś czas temu zastanawiałem się, czy bezpiecznie jest używać domen do obsługi wyjątków na poziomie interfejsu API / funkcji. Chciałem ich użyć do uproszczenia kodu obsługi wyjątków w każdej funkcji asynchronicznej, którą napisałem. Obawiałem się, że użycie nowej domeny dla każdej funkcji spowoduje znaczne obciążenie. Moja praca domowa wydaje się wskazywać, że narzut jest minimalny i że wydajność jest w rzeczywistości lepsza w domenach niż w niektórych sytuacjach przy próbie złapania.

http://www.lighthouselogic.com/#/using-a-new-domain-for-each-async-function-in-node/

Sudsy
źródło
1

Błędy łapania zostały tutaj bardzo dobrze omówione, ale warto pamiętać, aby gdzieś je wylogować, aby można je było wyświetlić i naprawić.

Bunyan jest popularnym środowiskiem rejestrowania dla NodeJS - obsługuje pisanie w wielu różnych miejscach wyjściowych, co czyni go użytecznym do lokalnego debugowania, o ile unika się pliku console.log. W module obsługi błędów swojej domeny możesz wyrzucić błąd do pliku dziennika.

var log = bunyan.createLogger({
  name: 'myapp',
  streams: [
    {
      level: 'error',
      path: '/var/tmp/myapp-error.log'  // log ERROR to this file
    }
  ]
});

Może to zająć dużo czasu, jeśli masz wiele błędów i / lub serwerów do sprawdzenia, więc warto przyjrzeć się narzędziu, takim jak Raygun (zastrzeżenie, pracuję w Raygun), aby zgrupować błędy razem - lub użyć ich obu razem. Jeśli zdecydowałeś się użyć Raygun jako narzędzia, konfiguracja jest również dość łatwa

var raygunClient = new raygun.Client().init({ apiKey: 'your API key' });
raygunClient.send(theError);

Twoja aplikacja powinna zostać zawieszona przy użyciu narzędzia takiego jak PM2 lub na zawsze, powinna się zawiesić, wylogować, co się stało, i uruchomić ponownie bez większych problemów.

K. Craven
źródło