Pisanie programów, które działają, gdy wszystko się udaje jest dobre na początek. Jednak prawdziwe wyzwanie to napisać program, który potrafi odpowiednio się zachować, gdy wystąpią jakieś niespodziewane zdarzenia.
Wyróżnia się dwa rodzaje trudnych sytuacji, w jakich może znaleźć się program: spowodowane błędem programisty i przez czynniki zewnętrzne. Przykładem pierwszego rodzaju problemów jest niepodanie funkcji wymaganego argumentu. Natomiast czynnikiem zewnętrznym niezależnym od programisty jest np. sytuacja, gdy program wymaga podania hasła, a zamiast niego otrzymuje pusty łańcuch.
Ogólnie rzecz biorąc błędy programistyczne po prostu trzeba znaleźć i poprawić. Natomiast błędy spowodowane czynnikami zewnętrznymi należy przewidzieć, aby opracować algorytmy pozwalające programowi wyjść z trudnych sytuacji (np. ponownie wyświetlając prośbę o podanie imienia) albo przynajmniej zakończyć działanie w elegancki i kontrolowany sposób.
Ważne jest, aby umieć oceniać, do której kategorii należy dany błąd. Weźmy np. naszą starą funkcję power
:
function power(base, exponent) {
var result = 1;
for (var count = 0; count < exponent; count++)
result *= base;
return result;
}
Gdy jakiś wariat spróbuje wykonać wywołanie power("Królik", 4)
, to jest to oczywiście błąd programistyczny, ale czy wywołanie power(9, 0.5)
też nim jest? Nasza funkcja nie obsługuje potęg ułamkowych, ale w matematyce takie potęgowanie jest jak najbardziej dozwolone (funkcja Math.pow
też je obsługuje). Jeśli nie ma całkowitej jasności co do tego, jakie wartości przyjmuje funkcja, zazwyczaj dobrym posunięciem jest wypisanie przyjmowanych argumentów w komentarzu.
Błędy
Co powinna zrobić funkcja, gdy napotka problem, którego sama nie może rozwiązać? W rozdziale 4 napisaliśmy funkcję o nazwie between
:
function between(string, start, end) {
var startAt = string.indexOf(start) + start.length;
var endAt = string.indexOf(end, startAt);
return string.slice(startAt, endAt);
}
Jeśli ciągi start
i end
nie zostaną znalezione w łańcuchu, funkcja indexOf
zwróci -1
i funkcja between
zwróci same bzdury: wywołanie between("Your mother!", "{-", "-}")
zwróci "our mother"
.
Gdy w czasie działania programu funkcja zostanie wywołana w taki sposób, kod który ją wywołał otrzyma łańcuch zgodnie z oczekiwaniami i będzie na nim dalej operował. Jednak łańcuch zwrócony przez funkcję jest nieprawidłowy i wynik działań na nim wykonywanych również będzie niepoprawny. A jeśli będziesz mieć pecha, błąd ten ujawni się dopiero po tym, jak zostanie wykonanych kolejnych 20 funkcji. Znalezienie przyczyny problemów w takiej sytuacji jest bardzo trudne.
W niektórych rzadkich przypadkach można sobie darować sprawdzanie, czy funkcja działa prawidłowo. Jeśli np. wiadomo, że funkcja będzie wywoływana tylko w kilku miejscach i w każdym z nich otrzyma poprawne dane wejściowe, to zazwyczaj nie ma sensu trudzić się i rozbudowywać funkcję o niepotrzebne mechanizmy zachowania w trudnych sytuacjach.
Jednak najczęściej funkcje, które w żaden sposób nie informują o błędach są trudne w użyciu, a nawet niebezpieczne. Co by było, gdyby w kodzie wywołującym funkcję between
chciano sprawdzić, czy wszystko poszło dobrze? Nie da się tego zrobić, chyba że zrobi się jeszcze raz to samo, co zrobiła funkcja between
i porówna otrzymany wynik z wynikiem zwróconym przez tę funkcję. Tak nie powinno być. Jednym z możliwych rozwiązań jest sprawienie, aby funkcja between
zwracała jakąś specjalną wartość, np. false
albo undefined
, gdy wystąpi błąd w jej działaniu.
function between(string, start, end) {
var startAt = string.indexOf(start);
if (startAt == -1)
return undefined;
startAt += start.length;
var endAt = string.indexOf(end, startAt);
if (endAt == -1)
return undefined;
return string.slice(startAt, endAt);
}
Nietrudno zauważyć, że kod wychwytujący błędy raczej nie dodaje funkcjom urody. Ale teraz w kodzie, który wywoła funkcję between
można napisać coś takiego:
var input = prompt("Powiedz mi coś", "");
var parenthesized = between(input, "(", ")");
if (parenthesized != undefined)
print("Napisałeś w nawiasie „", parenthesized, "”.");
Zwracanie specjalnej wartości
Czasami zwrócenie specjalnej wartości jest idealnym rozwiązaniem na wypadek wystąpienia błędu. Metoda ta ma jednak wady. Po pierwsze funkcja może i bez tego zwracać wszystkie możliwe wartości. Spójrz np. na poniższą funkcję, która pobiera ostatni element z tablicy:
function lastElement(array) {
if (array.length > 0)
return array[array.length - 1];
else
return undefined;
}
show(lastElement([1, 2, undefined]));
Czy tablica miała ostatni element? Po samej wartości zwróconej przez funkcję lastElement
nie można się o tym dowiedzieć.
Druga wada metody zwracania specjalnej wartości jest to, że jej zastosowanie może powodować bałagan. Jeśli w jakimś miejscu funkcja between
zostanie wywołana 10 razy, to trzeba będzie 10 razy sprawdzić, czy została zwrócona wartość undefined
. Ponadto, jeśli funkcja between
zostanie wywołana przez inną funkcję nie mającą mechanizmu ochronnego przed awarią, będzie musiała sprawdzić wartość zwrotną funkcji between
, i jeśli będzie nią undefined
, funkcja ta może zwrócić wywołującemu undefined
lub jakąś inną specjalną wartość.
Czasami, gdy wydarzy się coś dziwnego, najlepszym rozwiązaniem jest natychmiastowe wstrzymanie dalszych działań i przejście w miejsce zawierające algorytm pozwalający rozwiązać ten problem.
Na szczęście konstrukcje tego typu występują w wielu językach programowania. Ogólnie techniki te nazywają się obsługą błędów.
Słowo kluczowe throw
Teoretycznie obsługa błędów polega na zgłaszaniu przez kod (ang. raise lub throw) wyjątków, które są wartościami. Zgłaszanie wyjątków to trochę jak turbodoładowany zwrot wartości przez funkcję — następuje nie tylko wyjście z bieżącej funkcji, ale i z kodu wywołującego aż do najwyższego poziomu, gdzie rozpoczęła się bieżąca ścieżka wykonywania. Proces ten nazywa się rozwijaniem stosu. Przypomnij sobie stos wywołań, o którym była mowa w rozdziale 3. Wyjątek przebiega przez cały ten stos i odrzuca po drodze wszystkie napotkane konteksty wywołań.
Gdyby wyjątek przechodził przez stos bez żadnych przeszkód, nie byłby przydatny i stanowiłby jedynie nowatorski sposób wywoływania awarii w programie. Na szczęście w różnych miejscach stosu na wyjątki można zastawiać pułapki. Służą do tego klauzule catch, które pozwalają przechwycić wyjątek i podjąć w związku z tym jakieś czynności, po wykonaniu których program może kontynuować działanie od miejsca, w którym wyjątek został przechwycony.
Na przykład:
function lastElement(array) {
if (array.length > 0)
return array[array.length - 1];
else
throw "Nie można pobrać ostatniego elementu pustej tablicy.";
}
function lastElementPlusTen(array) {
return lastElement(array) + 10;
}
try {
print(lastElementPlusTen([]));
}
catch (error) {
print("Coś poszło nie tak: ", error);
}
throw
to słowo kluczowe służące do zgłaszania wyjątków. Za pomocą słowa kluczowego try
zastawia się pułapki na wyjątki: jeśli kod znajdujący się za nim zgłosi wyjątek, zostanie wykonany blok kodu w klauzuli catch
. Zmienna, której nazwa znajduje się w nawiasie za słowem catch
jest nazwą wartości wyjątku wewnątrz tego bloku.
Zwróć uwagę, że w funkcji lastElementPlusTen
kompletni zignorowano to, że wykonywanie funkcji lastElement
mogłoby się nie powieść. Jest to wielka zaleta wyjątków — kod obsługi błędów jest potrzebny tylko w miejscu wystąpienia błędu i jego obsługi. W funkcjach znajdujących się pomiędzy nie trzeba się tym przejmować.
No, może prawie.
Rozważmy następujący przykład: funkcja o nazwie processThing
chce sprawić, aby podczas jej wykonywania zmienna najwyższego poziomu currentThing
wskazywała określoną wartość, aby inne funkcje również miały dostęp do tej wartości. Normalnie oczywiście wartość tę przekazałoby się jako argument, ale przyjmij na chwilę, że byłoby to niepraktyczne. Gdy funkcja zakończy działanie, zmienna currentThing
powinna zostać ustawiona z powrotem na null
.
var currentThing = null;
function processThing(thing) {
if (currentThing != null)
throw "O, nie! Już coś przetwarzamy!";
currentThing = thing;
/* jakieś skomplikowane operacje... */
currentThing = null;
}
A co będzie, jeśli wyjątek zostanie zgłoszony w trakcie wykonywania tych skomplikowanych operacji? Wówczas wywołanie funkcji processThing
zostanie wyrzucone ze stosu przez wyjątek i zmienna currentThing
nie zostanie z powrotem ustawiona na null
.
Słowa kluczowe try i finally
Po instrukcjach try
może znajdować się dodatkowo słowo kluczowe finally
określające blok kodu, który ma zostać wykonany po próbie wykonania bloku try
bez względu na to, co się stanie. Jeśli funkcja musi coś po sobie uporządkować, to ten kod porządkujący powinien właśnie być umieszczony w bloku finally
:
function processThing(thing) {
if (currentThing != null)
throw "O, nie! Już coś przetwarzamy!";
currentThing = thing;
try {
/* jakieś skomplikowane operacje... */
}
finally {
currentThing = null;
}
}
W programach JavaScript występuje wiele różnych błędów, które powodują zgłoszenie wyjątków przez środowisko. Na przykład:
try {
print(Sasquatch);
}
catch (error) {
print("Wyjątek: " + error.message);
}
W takich przypadkach zgłaszane są specjalne obiekty wyjątków. Każdy z nich ma własność message
zawierającą opis problemu. Podobne obiekty można tworzyć za pomocą słowa kluczowego new
i konstruktora Error
:
throw new Error("Pożar!");
Jeśli wyjątek przejdzie przez cały stos i nic go po drodze nie przechwyci, to zostanie obsłużony przez środowisko. Obsługa ta w każdej przeglądarce może być inna. Niektóre aplikacje mogą zapisywać informacje o błędzie w dzienniku, a inne wyświetlać okno z opisem błędu.
Błędy powodowane przez kod wpisany w konsoli na tej stronie są przechwytywane przez konsolę i wyświetlane wraz z innymi wynikami.
Wyjątki
Dla większości programistów wyjątki to nic więcej, jak mechanizm obsługi błędów. Jednak w istocie są one kolejnym sposobem sterowania wykonywaniem programu. Można je np. wykorzystać jako rodzaj instrukcji break
w funkcjach rekurencyjnych. Poniżej znajduje się kod dość dziwnej funkcji, która sprawdza czy obiekt, i znajdujące się w jego wnętrzu obiekty, zawiera przynajmniej siedem wartości true
:
var FoundSeven = {};
function hasSevenTruths(object) {
var counted = 0;
function count(object) {
for (var name in object) {
if (object[name] === true) {
counted++;
if (counted == 7)
throw FoundSeven;
}
else if (typeof object[name] == "object") {
count(object[name]);
}
}
}
try {
count(object);
return false;
}
catch (exception) {
if (exception != FoundSeven)
throw exception;
return true;
}
}
Wewnętrzna funkcja count
jest rekurencyjnie wywoływana dla każdego obiektu będącego częścią argumentu. Gdy wartość zmiennej counted
dojdzie do siedmiu, nie ma sensu kontynuować liczenia, ale sam zwrot z bieżącego wywołania funkcji count
niekoniecznie zatrzyma liczenie, ponieważ pod nim mogą być jeszcze inne wywołania. Dlatego użyliśmy instrukcji throw, która powoduje wyjście z wywołań funkcji count
i przejście do bloku catch
.
Jednak zwrócenie jedynie wartości true
w przypadku wyjątku jest niepoprawne. Coś innego mogłoby pójść nie tak i dlatego najpierw sprawdzamy, czy wyjątek jest utworzonym specjalnie na tę okazję obiektem FoundSeven
. Jeśli nie, ten blok catch
nie wie, jak go obsłużyć, a więc ponawia jego zgłoszenie.
W ten sposób często działa się też przy obsłudze błędów ― blok catch
powinien obsługiwać tylko te wyjątki, które potrafi obsłużyć. Zwracanie wartości łańcuchowych za pomocą instrukcji throw, jak w niektórych pokazanych w tym rozdziale przykładach, rzadko kiedy jest dobrym pomysłem, ponieważ trudno jest rozpoznać typ wyjątku. Lepszym pomysłem jest zwracanie niepowtarzalnych wartości, jak np. obiekt FoundSeven
albo wprowadzenie nowego typu obiektów, o czym będzie mowa w rozdziale 8.