Стратегии обработки ошибок

Части 1 и 2: ссылка В первой части мы поговорили о разных стратегиях обработки ошибок и о том, когда их рекомендуется применять. В частности, я рассказал, что предусловия функций должны проверяться с...

image

Части 1 и 2: ссылка

В первой части мы поговорили о разных стратегиях обработки ошибок и о том, когда их рекомендуется применять. В частности, я рассказал, что предусловия функций должны проверяться с помощью отладочных утверждений (debug assertions), т. е. только в режиме отладки.

Для проверки условия библиотека С предоставляет макрос assert(), но только если не определён NDEBUG. Однако, как и в случае со многими другими вещами в С, это простое, но иногда неэффективное решение. Главная проблема, с которой я столкнулся, — глобальность решения: у вас есть утверждения либо везде, либо нигде. Плохо это потому, что вы не сможете отключить утверждения в библиотеке, оставив их только в собственном коде. Поэтому многие авторы библиотек самостоятельно пишут макросы утверждений, раз за разом.

Давайте вместо этого создадим своё, более качественное решение, чтобы часть его можно было использовать многократно.

Исходный код.

Проблема с assert()

Хотя assert() хорошо делает свою работу, у этого решения есть ряд проблем:

  1. Невозможно задать дополнительное сообщение, предоставляющее больше информации об условии сбоя (failed condition). Отображается только преобразованное в строку выражение. Это позволяет делать хаки вродеassert(cond && !"my message"). Дополнительное сообщение могло бы быть полезным, если само по себе условие не даёт достаточно информации, наподобие assert(false). Более того, иногда нужно передавать и дополнительные параметры.
  2. Глобальность: либо все утверждения активны, либо ни одно не активно. Нельзя управлять утверждениями для какого-то отдельного модуля.
  3. Содержимое сообщения и способ его вывода определяются реализацией. А ведь вы можете захотеть управлять им или даже интегрировать в свой код журналирования.
  4. Не поддерживаются уровни утверждений. Некоторые из утверждений дороже других, так что иногда требуется более тонкое управление.
  5. Здесь используются макросы, причём один даже в нижнем регистре (lower-case)! Макросы — не лучшая вещь, их применение лучше минимизировать.

Давайте напишем универсальный усовершенствованный assert().

Первый подход

Так выглядит первый дубль. Вероятно, таким же образом вы пишете свои собственные макросы утверждений:

struct source_location
{
    const char* file_name;
    unsigned line_number;
    const char* function_name;
};

#define CUR_SOURCE_LOCATION source_location{__FILE__, __LINE__, __func__}

void do_assert(bool expr, const source_location& loc, const char* expression)
{
    if (!expr)
    {
        // handle failed assertion
        std::abort();
    }
}

#if DEBUG_ASSERT_ENABLED
    #define DEBUG_ASSERT(Expr) 
        do_assert(expr, CUR_SOURCE_LOCATION, #Expr)
#else
    #define DEBUG_ASSERT(Expr)
#endif

Я определил вспомогательную структуру struct, которая содержит информацию о местонахождении в коде (source location). При этом саму работу выполняет функция do_assert(), а макрос просто переадресует.

Это позволяет избежать трюков с do ... while(0). Размер макросов должен быть как можно меньше.

Теперь у нас есть макрос, который просто получает текущее местонахождение в коде (source location), используемое в макросе утверждения. С помощью настройки макроса DEBUG_ASSERT_ENABLED можно включать и отключать утверждения.

Возможная проблема: предупреждение о неиспользуемой переменной

Если вы когда-либо компилировали релизную сборку с включёнными предупреждениями, то знаете, что из-за любой переменной, которая использовалась только в утверждении, появится предупреждение «неиспользованная переменная» (unused variable).

Вы можете попытаться это предотвратить, написав не-утверждение (non-assertion) вроде:

#define DEBUG_ASSERT(Expr) (void)Expr

Не делайте так!

Я совершил подобную ужасную ошибку. В этом случае выражение будет вычислено даже при отключённых утверждениях. И если оно достаточно сложное, то это приведёт к большим потерям производительности. Взгляните на код:

iterator binary_search(iterator begin, iterator end, int value)
{
    assert(is_sorted(begin, end));
    // binary search
}

is_sorted() — это линейная операция, в то время как binary_search() имеет временную сложность O(log n). Даже при отключённых утверждениях is_sorted() всё ещё может вычисляться компилятором, потому что нет доказательств отсутствия его побочных эффектов!

Когда я совершил такую ошибку, то получил очень похожую ситуацию. Производительность сильно упала.

Но в любом случае DEBUG_ASSERT() не сильно лучше, чем assert(), так что остановимся на нём.

Внедряем настраиваемость и модульность

Проблемы номер 2 и 3 можно решить с помощью политики (policy). Это дополнительный шаблонный параметр, управляющий активацией утверждения и способом вывода сообщения на экран. В каждом модуле, в котором требуется обеспечить отдельное управление утверждениями, нужно определить свой собственный Handler:

template <class Handler>
void do_assert(bool expr, const source_location& loc, const char* expression) noexcept
{
    if (Handler::value && !expr)
    {
        // handle failed assertion
        Handler::handle(loc, expression);
        std::abort();
    }
}

#define DEBUG_ASSERT(Expr, Handler) 
    do_assert<Handler>(Expr, CUR_SOURCE_LOCATION, #Expr)

Вместо жёсткого прописывания в коде способа вычисления выражения мы вызываем функцию static handle() применительно к конкретному Handler.

Чтобы предотвратить бросания исключений Handler’ом при покидании функции, я сделал do_assert() noexcept, а для возвратов функции обработчика сделал вызов std::abort().

Функция также управляет проверкой выражения с помощью константы value (std::true_type/std::false_type). Теперь макрос утверждения безоговорочно переадресует в do_assert().

Однако у этого кода тот же недостаток, что описан выше: выражение вычисляется всегда, когда выполняется ветка Handler::value!

Вторая проблема решается легко: Handler::value — это константа, поэтому мы можем воспользоваться эмуляцией constexpr if. Но как предотвратить вычисление выражения? Пойдём на хитрость — используем лямбду:

template <class Handler, class Expr>
void do_assert(std::true_type, const Expr& e, const source_location& loc, const char* expression) noexcept
{
    if (!e())
    {
        Handler::handle(loc, expression);
        std::abort();
    }
}

template <class Handler, class Expr>
void do_assert(std::false_type, const Expr&, const source_location&, const char*) noexcept {}

template <class Handler, class Expr>
void do_assert(const Expr& e, const source_location& loc, const char* expression)
{
    do_assert<Handler>(Handler{}, e, loc, expression);
}

#define DEBUG_ASSERT(Expr, Handler) 
    do_assert<Handler>([&] { return Expr; }, CUR_SOURCE_LOCATION, #Expr)

Теперь этот код считает, что Handler наследует от std::true_type или std::false_type.

Чтобы реализовать статическую диспетчеризацию (static dispatch), мы делаем здесь «классическую» теговую диспетчеризацию (tag dispatching). Но что ещё важнее, мы изменили обработку выражения: вместо прямой передачи выражения bool (что означает вычисление выражения) макрос создаёт лямбду, которая возвращает выражение. Теперь оно будет вычисляться только при вызове лямбды.

  • Это выполняется только при включённых утверждениях.

Трюк с обёртыванием в лямбду ради откладывания вычисления полезен во всех ситуациях, когда у вас исключительно опциональные проверки, а вы не хотите использовать макросы. Например, в memory я применяю этот подход для проверок на двойное освобождение ресурсов (double deallocation).

Есть ли здесь какие-то издержки?

Макрос постоянно активен, так что он всегда будет вызывать функцию do_assert(). Для сравнения, при условном компилировании (conditional compilation) макрос работает вхолостую. Так есть ли какие-то издержки?

Я тщательно проанализировал несколько компиляторов. При компилировании с выключенными оптимизациями мы имеем только вызов do_assert(), который переадресуется в неоптимизированную версию. Выражение остаётся нетронутым, и уже на начальном уровне оптимизаций вызов полностью устраняется.

Я хотел улучшить генерирование кода при отключённых оптимизациях, поэтому включил SFINAE, чтобы выбрать перегрузку вместо теговой диспетчеризации. Благодаря этому отпадает необходимость в функции-трамплине, которая вставляет тег. Теперь макрос напрямую вызывает неоптимизированную версию. Я также пометил, чтобы он принудительно встраивался (force-inline), так что компилятор будет это делать даже без оптимизаций. Всё, что он делает, — это создаёт объект source_location.

Но, как и прежде, при любых оптимизациях макрос как будто работает вхолостую.

Добавление уровней утверждений

При таком подходе очень легко добавлять другие уровни утверждений:

template <class Handler, unsigned Level, class Expr>
auto do_assert(const Expr& expr, const source_location& loc, const char* expression) noexcept
-> typename std::enable_if<Level <= Handler::level>::type
{
    static_assert(Level > 0, "level of an assertion must not be 0");
    if (!expr())
    {
        Handler::handle(loc, expression);
        std::abort();
    }
}

template <class Handler, unsigned Level, class Expr>
auto do_assert(const Expr&, const source_location&, const char*) noexcept
-> typename std::enable_if<(Level > Handler::level)>::type {}

#define DEBUG_ASSERT(Expr, Handler, Level) 
    do_assert<Handler, Level>([&] { return Expr; }, CUR_SOURCE_LOCATION, #Expr)

Также здесь вместо тегов используется SFINAE.

При определении активированности утверждений вместо Handler::value теперь включается условие Level <= Handler::level. Чем выше уровень, тем больше утверждений активируется. Уровень 0 означает, что не выполняются никакие утверждения.

Обратите внимание: это также означает, что минимальный уровень частичного утверждения — 1.

Последний шаг: добавляем сообщение

Сделать это очень просто: вписываем дополнительный параметр, который будет передаваться обработчику. Но иногда нам не нужно, чтобы утверждения содержали сообщение, потому что условия и так дают достаточно информации. Так что хорошо бы перегружать макрос. Увы, такой возможности нет. То же самое и с уровнями: может быть, вам не захочется определять их каждый раз. Более того, поскольку обработчик является дженериком, то он способен принимать дополнительные аргументы.

Так что нам нужен макрос утверждения, который сможет обработать любое количество аргументов. То есть макрос с переменным количеством аргументов (variadic):

template <unsigned Level>
using level = std::integral_constant<unsigned, Level>;

// overload 1, with level, enabled
template <class Expr, class Handler, unsigned Level, typename ... Args>
auto do_assert(const Expr& expr, const source_location& loc, const char* expression,
               Handler, level<Level>,
               Args&&... args) noexcept
-> typename std::enable_if<Level <= Handler::level>::type
{
    static_assert(Level > 0, "level of an assertion must not be 0");
    if (!expr())
    {
        Handler::handle(loc, expression, std::forward<Args>(args)...);
        std::abort();
    }
}

// overload 1, with level, disabled
template <class Expr, class Handler, unsigned Level, typename ... Args>
auto do_assert(const Expr&, const source_location&, const char*,
               Handler, level<Level>,
               Args&&...) noexcept
-> typename std::enable_if<(Level > Handler::level)>::type {}

// overload 2, without level, enabled
template <class Expr, class Handler, typename ... Args>
auto do_assert(const Expr& expr, const source_location& loc, const char* expression,
               Handler,
               Args&&... args) noexcept
-> typename std::enable_if<Handler::level != 0>::type
{
    if (!expr())
    {
        Handler::handle(loc, expression, std::forward<Args>(args)...);
        std::abort();
    }
}

// overload 2, without level, disabled
template <class Expr, class Handler, typename ... Args>
auto do_assert(const Expr&, const source_location&, const char*,
               Handler,
               Args&&...) noexcept
-> typename std::enable_if<Handler::level == 0>::type {}

#define DEBUG_ASSERT(Expr, ...) 
    do_assert([&] { return Expr; }, CUR_SOURCE_LOCATION, #Expr, __VA_ARGS__)

У нас есть два параметра, которые необходимо задать: выражение и обработчик. Поскольку макрос-вариадик не может быть пустым, мы именуем только первый требуемый параметр. Все параметры вариадика передаются в качестве параметров для вызова функции.

Это вносит некоторые изменения в характер использования: перед Handler может идти имя типа и константа Level, и теперь их нужно настраивать, потому что они являются параметрами регулярной функции. Handler должен быть объектом типа обработчика, и Level, и объектом типа level<N>. Это позволяет сделать дедукцию аргумента (argument deduction) для вычисления подходящих параметров.

Также вышеприведённый код поддерживает любое количество дополнительных аргументов, которые просто переадресуются функции-обработчику. Я хочу разрешить следующие варианты вызова:

  1. DEBUG_ASSERT(expr, handler{}) — без уровня, без дополнительных аргументов.
  2. DEBUG_ASSERT(expr, handler{}, level<4>{}) — с уровнем, но без дополнительных аргументов.
  3. DEBUG_ASSERT(expr, handler{}, msg) — без уровня, но с дополнительным аргументом (сообщение).
  4. DEBUG_ASSERT(expr, handler{}, level<4>{}, msg) — с уровнем и дополнительным аргументом (сообщение).

Чтобы это реализовать, нам нужно две перегрузки (overloads) do_assert(). Первая обрабатывает все перегрузки с уровнем (2 и 4), вторая — без (1 и 3).

Но это всё ещё макрос!

Одной из проблем assert() является то, что это макрос. Да, всё ещё макрос!

Но нужно отметить и серьёзное улучшение: нам больше не требуется макрос для отключения утверждения. Теперь он нужен только для трёх вещей. Чтобы:

  1. Получить текущее местонахождение в коде (source location).
  2. Преобразовать выражение в строку.
  3. Преобразовать выражение в лямбду, чтобы включить отложенное вычисление.

Что касается 1, то в Library Fundamentals V2 есть std::experimental::source_location. Этот класс представляет расположение исходного кода, как написанная мной структура struct. Но за его извлечение во время компилирования отвечают не макросы, а статическая функция класса — current(). Более того, если использовать этот класс таким образом:

void foo(std::experimental::source_location loc = std::experimental::source_location::current());

то loc получит местонахождение вызывающего фрагмента кода, а не параметра! Это именно то, что нужно для макросов утверждения.

К сожалению, во втором и третьем вариантах мы ничем не можем заменить макрос. Это нужно делать вручную посредством вызывающего фрагмента кода. Так что мы не избавимся от макроса, покуда нам нужна гибкость использования.

Промежуточное заключение

Мы создали простую утилиту для утверждений (assertion utility), гибкую в использовании, дженерик (generic) и поддерживающую отдельные уровни утверждений для каждого модуля. Во время написания этой статьи я решил опубликовать код в виде header-only библиотеки: debug-assert.

В ней вы найдёте дополнительный код, например легко генерируемые модульные обработчики:

struct my_module
: debug_assert::set_level<2>, // set the level, normally done via buildsystem macro
  debug_assert::default_handler // use the default handler
{};

Просто скопируйте заголовок в свой проект и начните пользоваться новым, улучшенным макросом утверждений. Надеюсь, он убережёт вас от написания макросов для каждого из проектов, в которых вам понадобится управлять утверждениями раздельно. На данный момент это очень маленькая и быстро созданная библиотека, так что, если у вас есть идеи о её модернизации, дайте мне знать!

Утверждения — полезный инструмент для проверки предусловий функций. Но правильная архитектура типов может предотвратить возникновение ситуаций, в которых нужно использовать утверждения. В С++ прекрасная система типов, так что давайте применять её себе во благо.

Мотивация

Я работаю над standardese, генератором документации C++. И там мне приходится иметь дело с большим количеством строковых значений. В частности, я постоянно удаляю пробелы в конце строк. Поскольку это очень простая задача, а определение пробела варьируется в зависимости от ситуации, я не озаботился написанием для этого отдельной функции.

Оглядываясь назад, могу сказать, что следовало бы.

Я использую подобный код:

while (is_whitespace(str.back())
    str.pop_back();

Пишу две строки, коммичу, выполняю push и, привычно дождавшись, когда сработает CI, получаю письмо с сообщением о сбое в Windows-сборке. Я в недоумении: у меня на машине всё работало, как и во всех Linux- и MacOS-сборках! Смотрю лог: тестовое исполнение завершилось тайм-аутом.

Запускаю Windows и собираю там проект. При запуске тестов получаю удивительно скомпонованный диалог о сбое отладочных утверждений.

Тот самый, в котором Retry означает Debug.

Смотрю сообщение об ошибке. Рукалицо. Коммичу фикс:

while (!str.empty() && is_whitespace(str.back())
    str.pop_back();

Иногда строка бывает пустой. В libstdc++ в подобных случаях по умолчанию не включаются утверждения, что приводит к закономерному результату. Но в MSVC утверждения включены, и он замечает такие случаи.

Я совершил эту ошибку трижды. Всё-таки надо было написать функцию.

Также возникло ещё несколько проблем: я не следовал принципу DRY, libstdc++ по умолчанию не проверяла предусловия, Appveyor’у не нравятся графические диалоги утверждений, а MSVC под Linux не существует.

Но я считаю, что главную роль в произошедшем сыграла архитектура std::string::back(). Если бы этот класс был сделан по уму, то код бы не скомпилировался и система не напомнила мне о том факте, что строка может быть пустой. Это сэкономило бы 15 минут моей жизни и одну загрузку в Windows.

Как можно было этого избежать? С помощью системы типов.

Решение

Рассматриваемая функция имеет такую упрощённую сигнатуру (signature):

char& back();

Она возвращает последний символ строки. Если строка пустая, то в ней просто нет последнего символа, а значит, её вызов в любом случае является неопределённым поведением. Как нам об этом узнать? Если подумать, то всё очевидно: какой char должен быть возвращён в случае пустой строки? Здесь нет «неправильного» char, так что какой попало не вернёт.

На самом деле это , но в то же время это и последний символ std::string, и вы не сможете различить их.

Но об этом я не думал. Моя голова была занята сложным алгоритмом парсинга комментариев и проблемой того, что некоторые разработчики оставляют в конце комментариев пробелы, ломающие весь последующий парсинг разметки!

У back() есть узкий контракт (narrow contract) — предусловие. Без сомнения, труднее работать с функциями с узким контрактом, чем с широким (wide contract). Так что одной из возможных задач может быть такая: сделать как можно меньше узких контрактов.

Одна из проблем функции back() — то, что в ней не предусмотрено валидного возвращаемого символа на случай пустой строки. Но в С++ 17 есть потенциально полезное дополнение: std::optional:

std::optional<char> back();

std::optional может содержать значение, а может и не содержать. Если строка не пустая, то back() возвращает optional, содержащий последний символ. Но если строка пустая, то функция может вернуть optional, равный null. То есть мы смоделировали функцию так, что теперь нам больше не нужны предусловия.

Обратите внимание: при этом мы потеряли возможность использовать back() в качестве l-значения, потому что теперь нельзя применять std::optional<T&>. Так что std::optional — не самое лучшее решение, но об этом ниже.

Предположим, что std::string::back() имеет такую сигнатуру. Я снова сосредоточился на коде парсинга комментариев и написании пары строк для быстрого стирания «висящих» пробелов:

while (is_whitespace(str.back())
    str.pop_back();

is_whitespace() берёт char, но back() возвращает std::optional, так что я немедленно получаю на своей машине ошибку компилирования. Компилятор выловил для меня возможный баг, причём статически, с помощью одной лишь системы типов! Мне автоматически напомнили, что строка может быть пустой и что мне нужно приложить дополнительные усилия для получения символа.

Конечно, я всё ещё могу ошибиться, ведь std::optional на самом деле не предназначен для этой задачи:

while (is_whitespace(*str.back())

Этот код ведёт себя точно так же, и, вероятно, в MSVC появится отладочное утверждение. std::optional<T>::operator* не должен вызываться при optional = null, он возвращает содержащееся в нём значение. Так будет чуть лучше:

while (is_whitespace(str.back().value())

По крайней мере, std::optional<T>::value() предназначен для бросания исключения при optional = null, так что как минимум будет устойчиво сбоить в ходе runtime. Но оба этих решения не имеют абсолютно никаких преимуществ по сравнению с кодом с той же сигнатурой. Эти компонентные функции (member functions) никуда не годятся, они пробивают бреши в замечательных абстракциях, они вообще не должны существовать! Вместо них лучше применять высокоуровневые функции, благодаря которым было бы необязательно запрашивать значение. А для случаев, когда это необходимо, нужно использовать не-компонентные функции (non-member functions) с длинными, примечательными именами, которые заставляют быть внимательнее, — а не просто с одиночной звёздочкой!

std::optional и впрямь не лучшее решение. Он был создан как альтернатива std::unique_ptr<T>, который не выделяет память, не больше и не меньше. Это тип-указатель (pointer type), а не монада «может быть» (Maybe), которой он мог бы быть. Из-за этого он бесполезен для решения ряда задач, когда нужны монады. Например, как эта.

Лучше воспользоваться таким решением:

while (is_whitespace(str.back().value_or(''))

std::optional<T>::value_or() возвращает либо значение, либо его альтернативу. В этом случае optional возвращает нулевой символ, который прекрасно подходит для прерывания цикла. Но, конечно, не всегда есть правильное недопустимое значение. Так что идеальным вариантом было бы изменить сигнатуру is_whitespace() так, чтобы она принимала std::optional<char>.

Руководство 1: используйте правильный тип возвращаемого значения

Есть много функций, которые либо что-то возвращают, либо вообще не должны вызываться. К таким функциям относятся и back()/front(). Их можно настроить так, чтобы они возвращали опциональный тип (optional type) вроде std::optional<T>. Затем нужно выполнить проверку предусловия, при этом сама система типов помогает избегать ошибок, а также облегчает их обнаружение и обработку.

Конечно, мы не можем применять std::optional<T> везде, где есть вероятность нарваться на ошибку. Некоторые ошибки не относятся к ошибкам предусловий. В подобных ситуациях надо бросать исключение или использовать что-то подобное предлагаемому std::expected<T, E>, который возвращает валидное значение или тип ошибки (error type). А если функции либо что-то возвращают, либо не должны вызываться при недопустимом состоянии, для них лучше возвращать опциональный тип.

Параметрические предусловия (parameter preconditions)

Мы разобрались с предусловиями для недопустимых состояний, но чаще всего предусловия связаны с параметрами. Однако, изменив тип параметра, вы также можете легко избавиться и от предусловия.

Рассмотрим функцию:

void foo(T* ptr)
{
    assert(ptr);
    …
}

Изменим сигнатуру на:

void foo(T& ref);

Теперь мы больше не можем передавать значение нулевого указателя (null pointer value). А если это всё же сделать, то вина за неопределённое поведение в связи с разыменованием (dereferencing) ляжет на вызывающих (callers).

Этот подход работает не только с простыми указателями:

void foo(int value)
{
    assert(value >= 0);
    …
}

Изменим сигнатуру на:

void foo(unsigned value);

Теперь мы не можем передавать отрицательное значение без потери значимости (underflow). К сожалению, С++ унаследовал от С неявное преобразование из типов со знаком в типы без знаков, так что решение не идеальное.

Руководство 2: используйте правильные типы аргументов

Выбирайте типы аргументов таким образом, чтобы можно было исключить предусловия и отразить их напрямую в коде. У вас есть указатель, который не должен быть null? Передайте ссылку. Целочисленное значение, которое не должно быть отрицательным? Передайте без знака. Целочисленное значение, которое может иметь лишь определённый именованный набор значений? Сделайте перечисление (enumeration).

Можно пойти ещё дальше и написать общий обёрточный тип (general wrapper type), чей — явный! — конструктор утверждает, что у «необработанного» (raw) значения есть определённое значение, например:

class non_empty_string
{
public:
    explicit non_empty_string(std::string str)
    : str_(std::move(str))
    {
        assert(!str_.empty());
    }

    std::string get() const
    {
        return str_;
    }

    … // other functions you might want

private:
    std::string str_;
};

Эту маленькую обёртку очень легко обобщить. Её использование выражает намерение и позволяет создать основную точку для проверки на валидность. Потом можно легко различать уже проверенные и, вероятно, недопустимые значения, а также безо всякой документации делать предусловия очевидными требованиями.

Конечно, такой подход не всегда возможен. Иногда по условиям соглашения нам требуется какой-то определённый тип. Кроме того, может быть нецелесообразно пытаться использовать этот подход повсеместно: если вам нужны определённые предусловия лишь в одном месте, то зачем для этого писать целый шаблон?

Заключение

Система типов в C++ достаточно мощна, чтобы помогать вам ловить ошибки.

Правильная архитектура функции позволяет перенести многие предусловия из самой функции в какое-то одно централизованное место. Выбирайте семантические типы аргументов, которые могут естественным образом выражать предусловия. Также выбирайте опциональные возвращаемые типы, если иногда функция не может вернуть валидное значение.

Добавлено 30 мая 2021 в 21:14

В уроке «7.14 – Распространенные семантические ошибки при программировании на C++» мы рассмотрели многие типы распространенных семантических ошибок, с которыми сталкиваются начинающие программисты при работе с языком C++. Если ошибка является результатом неправильного использования языковой функции или логической ошибки, исправить ее можно просто.

Но большинство ошибок в программе возникает не в результате непреднамеренного неправильного использования языковых функций – скорее, большинство ошибок возникает из-за ошибочных предположений, сделанных программистом, и/или из-за отсутствия надлежащего обнаружения/обработки ошибок.

Например, в функции, предназначенной для поиска оценки учащегося, вы могли предположить, что:

  • просматриваемый студент будет существовать;
  • имена всех студентов будут уникальными;
  • в предмете используется числовая оценка (вместо «зачет/незачет»).

Что, если какое-либо из этих предположений неверно? Если программист не предвидел этих случаев, программа при возникновении таких случаев, скорее всего, завершится со сбоем (обычно в какой-то момент в будущем, через долгое время после того, как функция была написана).

Есть три ключевых места, где обычно возникают ошибки предположений:

  • Когда функция возвращает значение, программист мог предположить, что вызов функции будет успешным, хотя это не так.
  • Когда программа получает входные данные (либо от пользователя, либо из файла), программист мог предположить, что ввод будет в правильном формате и семантически корректен, хотя это не так.
  • Когда функция была вызвана, программист мог предположить, что параметры будут семантически допустимыми, хотя это не так.

Многие начинающие программисты пишут код, а затем проверяют только счастливый путь: только те случаи, когда ошибок нет. Но вы также должны планировать и проверять печальные пути, на которых что-то может пойти и пойдет не так. В уроке «3.10 – Поиск проблем до того, как они станут проблемами», мы определили защитное программирование как попытку предвидеть все способы неправильного использования программного обеспечения конечными пользователями или разработчиками (либо самим программистом, либо другими). Как только вы ожидаете (или обнаруживаете) какое-то неправильное использование, следующее, что вам нужно сделать, – это обработать его.

В этом уроке мы поговорим о стратегиях обработки ошибок (что делать, если что-то пойдет не так) внутри функции. В следующих уроках мы поговорим о проверке ввода данных пользователем, а затем представим полезный инструмент, помогающий документировать и проверять предположения.

Обработка ошибок в функциях

Функции могут давать сбой по любому количеству причин – вызывающий мог передать аргумент с недопустимым значением, или что-то может дать сбой в теле функции. Например, функция, открывающая файл для чтения, может не работать, если файл не может быть найден.

Когда это произойдет, в вашем распоряжении будет несколько вариантов. Лучшего способа справиться с ошибкой нет – это на самом деле зависит от характера проблемы и от того, можно ли устранить проблему или нет.

Можно использовать 4 основные стратегии:

  • обработать ошибку в функции;
  • передать ошибку вызывающему, чтобы он разобрался с ней;
  • остановить программу;
  • выбросить исключение.

Обработка ошибки в функции

Если возможно, наилучшей стратегией является восстановление после ошибки в той же функции, в которой возникла ошибка, так, чтобы ошибку можно было локализовать и исправить, не влияя на какой-либо код вне функции. Здесь есть два варианта: повторять попытки до успешного завершения или отменить выполняемую операцию.

Если ошибка возникла из-за чего-то, не зависящего от программы, программа может повторять попытку, пока не будет достигнут успех. Например, если программе требуется подключение к Интернету, и пользователь потерял соединение, программа может отобразить предупреждение, а затем использовать цикл для периодической повторной проверки подключения к Интернету. В качестве альтернативы, если пользователь ввел недопустимые входные данные, программа может попросить пользователя повторить попытку и выполнять этот цикл до тех пор, пока пользователь не введет корректные входные данные. Мы покажем примеры обработки недопустимого ввода и использования циклов для повторных попыток в следующем уроке (7.16 – std::cin и обработка недопустимого ввода).

Альтернативная стратегия – просто игнорировать ошибку и/или отменить операцию. Например:

void printDivision(int x, int y)
{
    if (y != 0)
        std::cout << static_cast<double>(x) / y;
}

В приведенном выше примере, если пользователь ввел недопустимое значение для y, мы просто игнорируем запрос на печать результата операции деления. Основная проблема при этом заключается в том, что у вызывающей функции или у пользователя нет возможности определить, что что-то пошло не так. В таком случае может оказаться полезным напечатать сообщение об ошибке:

void printDivision(int x, int y)
{
    if (y != 0)
        std::cout << static_cast<double>(x) / y;
    else
        std::cerr << "Error: Could not divide by zeron";
}

Однако если вызывающая функция ожидает, что вызываемая функция выдаст возвращаемое значение или какой-либо полезный побочный эффект, тогда просто игнорирование ошибки может быть недопустимым вариантом.

Передача ошибок вызывающей функции

Во многих случаях обработать ошибку с помощью функции, которая ее обнаруживает, невозможно. Например, рассмотрим следующую функцию:

double doDivision(int x, int y)
{
    return static_cast<double>(x) / y;
}

Если y равно 0, что нам делать? Мы не можем просто пропустить логику программы, потому что функция должна возвращать какое-то значение. Мы не должны просить пользователя ввести новое значение для y, потому что это функция вычисления, и введение в нее процедур ввода может быть или не быть подходящим для программы, вызывающей эту функцию.

В таких случаях лучшим вариантом может быть передача ошибки обратно вызывающей функции в надежде, что вызывающая сторона сможет с ней справиться.

Как мы можем это сделать?

Если функция имеет тип возвращаемого значения void, его можно изменить, чтобы она возвращала логическое значение, указывающее на успех или на неудачу. Например, вместо:

void printDivision(int x, int y)
{
    if (y != 0)
        std::cout << static_cast<double>(x) / y;
    else
        std::cerr << "Error: Could not divide by zeron";
}

Мы можем сделать так:

bool printDivision(int x, int y)
{
    if (y == 0)
    {
        std::cerr << "Error: could not divide by zeron";
        return false;
    }
    
    std::cout << static_cast<double>(x) / y;
 
    return true;
}

Таким образом, вызывающий может проверить возвращаемое значение, чтобы узнать, не завершилась ли функция по какой-либо причине неудачей.

Если функция возвращает обычное значение, всё немного сложнее. В некоторых случаях полный диапазон возвращаемых значений не используется. В таких случаях, чтобы указать на ошибку, мы можем использовать возвращаемое значение, которое иначе было бы невозможно. Например, рассмотрим следующую функцию:

// Обратное (reciprocal) к x равно 1/x
double reciprocal(double x)
{
    return 1.0 / x;
}

Обратное к некоторому числу x определяется как 1/x, а число, умноженное на обратное, равно 1.

Однако что произойдет, если пользователь вызовет эту функцию как reciprocal(0.0)? Мы получаем ошибку деления на ноль и сбой программы, поэтому очевидно, что мы должны защититься от этого случая. Но эта функция должна возвращать значение doube, так какое же значение мы должны вернуть? Оказывается, эта функция никогда не выдаст 0.0 как допустимый результат, поэтому мы можем вернуть 0.0, чтобы указать на случай ошибки.

// Обратное (reciprocal) к x равно 1/x; возвращает 0.0, если x=0
double reciprocal(double x)
{
    if (x == 0.0)
       return 0.0;
 
    return 1.0 / x;
}

Однако если требуется полный диапазон возвращаемых значений, то использование возвращаемого значения для указания ошибки будет невозможно (поскольку вызывающий не сможет определить, является ли возвращаемое значение допустимым значением или значением ошибки). В таком случае выходной параметр (рассмотренный в уроке «11.3 – Передача аргументов по ссылке») может быть жизнеспособным вариантом.

Фатальные ошибки

Если ошибка настолько серьезна, что программа не может продолжать работать правильно, она называется неисправимой ошибкой (или фатальной ошибкой). В таких случаях лучше всего завершить программу. Если ваш код находится в main() или в функции, вызываемой непосредственно из main(), лучше всего позволить main() вернуть ненулевой код состояния. Однако если вы погрузились в какую-то глубоко вложенную подфункцию, передать ошибку обратно в main() может быть неудобно или невозможно. В таком случае можно использовать инструкцию остановки (например, std::exit()).

Например:

double doDivision(int x, int y)
{
    if (y == 0)
    {
        std::cerr << "Error: Could not divide by zeron";
        std::exit(1);
    }
    return static_cast<double>(x) / y;
}

Исключения

Поскольку возврат ошибки из функции обратно вызывающей функции сложен (и множество различных способов сделать это приводит к несогласованности, а несогласованность ведет к ошибкам), C++ предлагает совершенно отдельный способ передачи ошибок обратно вызывающей стороне: исключения.

Основная идея состоит в том, что при возникновении ошибки «выбрасывается» исключение. Если текущая функция не «улавливает» ошибку, то уловить ошибку есть шанс у вызывающей функции. Если вызывающая функция не обнаруживает ошибку, то обнаружить ошибку есть шанс у функции, вызвавшей вызывающую функцию. Ошибка постепенно перемещается вверх по стеку вызовов до тех пор, пока она не будет обнаружена и обработана (в этот момент выполнение продолжается в обычном режиме), или пока main() не сможет обработать ошибку (в этот момент программа завершится с ошибкой исключения).

Мы рассмотрим обработку исключений в главе 20 этой серии обучающих статей.

Теги

C++ / CppException / ИсключениеLearnCppstd::exit()Для начинающихОбнаружение ошибокОбработка ошибокОбучениеПрограммирование

Ошибка может возникать на разных уровнях компиляции, поэтому обработка ошибок важна для корректного выполнения кода. В основном существует пять стратегий восстановления после ошибок, а именно:

  1. Режим паники
  2. Восстановление уровня фазы
  3. Производство ошибок
  4. Глобальная коррекция
  5. Таблица символов

Режим паники:

Эта стратегия используется большинством методов синтаксического анализа. В этом методе обнаружения ошибки синтаксический анализатор отбрасывает входные символы по одному. Этот процесс продолжается до тех пор, пока не будет найден один из назначенных наборов синхронизирующих токенов. Синхронизирующие маркеры — это разделители, такие как точки с запятой или концы. Эти токены указывают на конец оператора ввода.

Таким образом, в режиме паники восстановление значительного объема входных данных проверяется на наличие дополнительных ошибок. Если в одном и том же утверждении меньше ошибок, то эта стратегия является лучшим выбором.

Пример:

C

// После int a, 5abcd , sum, $2 ; // парсер отбрасывает входные символы по одному.

Преимущество:

  1. Его легко использовать.
  2. Программа никогда не зацикливается.

Недостаток:

  1. Этот метод может привести к семантической ошибке или ошибке времени выполнения на следующих этапах.

Восстановление уровня фазы:

В этой стратегии при обнаружении ошибки синтаксический анализатор выполняет исходную коррекцию оставшихся входных данных. Он может заменить префикс оставшегося ввода некоторой строкой. Это на самом деле помогает синтаксическому анализатору продолжать свою работу. Первоначальным исправлением может быть замена запятой точкой с запятой, пропуск точки с запятой или установка отсутствующей точки с запятой. Этот тип исходной коррекции определяется разработчиком компилятора.

Примеры:

C

Преимущества: этот метод используется во многих компиляторах, исправляющих ошибки.

Недостатки: При замене программа не должна зацикливаться.

Производство ошибок:

Это требует хорошего знания общих ошибок, с которыми можно столкнуться, тогда мы можем дополнить грамматику соответствующего языка продуктами ошибок, которые генерируют ошибочные конструкции. Если во время синтаксического анализа используется производство ошибок, мы можем сгенерировать соответствующее сообщение об ошибке, чтобы указать ошибку, которая была распознана во входных данных. Этот метод чрезвычайно отличается в обслуживании, потому что, если мы изменим грамматику, то возникает необходимость изменить соответствующие постановки.

Пример: предположим, что входная строка abcd.

Грамматика: С-> А

А-> аА | бА | и | б

Б->кд

Входная строка не может быть получена с помощью приведенной выше грамматики, поэтому нам нужно добавить расширенную грамматику.

Грамматика: E->SB // ДОПОЛНИТЬ ГРАММАТИКУ

С->А

А-> аА| бА | и | б

Б->кд

Теперь можно получить строку abcd.

Преимущества:

  1. Синтаксические фазовые ошибки обычно восстанавливаются путем производства ошибок.

Недостатки:

  1. Метод очень сложен в обслуживании, потому что если мы изменим грамматику, то потребуется изменить соответствующую продукцию.
  2. Разработчикам сложно его поддерживать.

Глобальная коррекция:

Нам часто нужен такой компилятор, который делает очень мало изменений при обработке неправильной входной строки. Учитывая неправильную входную строку x и грамматику G , сам алгоритм может найти дерево синтаксического анализа для связанной строки y; таким образом, чтобы количество вставок, удалений и изменений токена, необходимых для преобразования x в y, было как можно меньше. Методы глобальной коррекции увеличивают требования к времени и пространству во время синтаксического анализа. Это просто теоретическая концепция.

Преимущества: он вносит очень мало изменений при обработке неправильной входной строки.

Недостатки: Это просто теоретическая концепция, которую невозможно реализовать.

Таблица символов:

При семантических ошибках ошибки восстанавливаются с помощью таблицы символов для соответствующего идентификатора, и если типы данных двух операндов несовместимы, компилятор автоматически выполняет преобразование типов.

Преимущества: он позволяет выполнять базовое преобразование типов, которое мы обычно делаем в реальных вычислениях.

Недостатки: Возможно только неявное преобразование типов.

элементы управления, связанные с источниками данных. Например, элемент управления DataGrid связан с коллекцией объектов. Привязка данных часто используется с шаблонами раздельного представления для связывания компонентов UI (Представлений) с презентаторами или контроллерами (компоненты логики представления) либо с моделью слоя представления или компонентами сущностей.

Поддержка привязки данных и ее реализация в каждой технологии UI разная. В общем, большинство технологий UI позволяют выполнять привязку элементов управления к объектам и спискам объектов. Но некоторые специальные технологий привязки данных могут потребовать реализации в источниках данных определенных интерфейсов и событий для обеспечения полной поддержки привязки данных, например, интерфейса INotifyPropertyChanged (Уведомление об изменении свойства) в WPF или IBindingList (Список привязки) в Windows Forms. При использовании шаблона раздельного представления логика представления и компоненты данных должны гарантированно поддерживать необходимые интерфейсы или события, чтобы обеспечить простоту привязку элементов управления UI к ним.

Обычно используются два типа привязки:

Односторонняя привязка. Изменения свойства источника приводят к автоматическому обновлению целевого свойства, но изменения целевого свойства не распространяются на исходное свойство. Такой тип привязки подходит для неявно доступных только для чтения элементов управления. Примером такой односторонней привязки могут быть биржевые сводки. Если нет необходимости отслеживать изменения целевого свойства, использование односторонней привязки позволит избежать ненужных издержек.

Двухсторонняя привязка. Изменения любого из свойств, исходного либо целевого, приводят к автоматическому обновлению второго свойства. Такой тип привязки подходит для редактируемых форм или других полностью интерактивных сценариев UI. Многие редактируемые элементы управления в Windows Forms, ASP.NET и WPF поддерживают двухстороннюю привязку, так что изменения источника данных отражаются в элементе управления UI, и изменения в элементе управления UI отражаются в источнике данных.

Компоненты UI являются внешней границей приложения, и поэтому должны реализовывать соответствующую стратегию обработки ошибок для обеспечения стабильности приложения и положительного впечатления при взаимодействии с пользователем. При проектировании стратегии обработки ошибок рассмотрите следующие варианты:

Стратегия централизованной обработки исключений. Обработка исключений и ошибок относится к сквозной функциональности. Она должна реализовываться в отдельных компонентах, которые обеспечивают централизацию этой функциональности и делают ее доступной во всех слоях приложения. Это также упрощает обслуживание и способствует повторному использованию.

Протоколирование исключений. Очень важно протоколировать ошибки на границах системы, чтобы служба поддержки могла выявлять и диагностировать их. Это важно для компонентов представления, но может создавать большие сложности для кода, выполняющегося на клиентских компьютерах. Будьте осторожны и тщательно выбирайте методы протоколирования информации личного порядка (Personally Identifiable Information, PII) или конфиденциальных данных и обратите особое внимание на размер и размещение журнала.

Вывод на экран понятных пользователю сообщений. При использовании этой стратегии в случае возникновения ошибки на экран выводится понятное пользователю сообщение с указанием причины ошибки и описанием, как ее можно исправить. Например, ошибки проверки данных должны отображаться так, чтобы было понятно, какие данные являются ошибочными и почему. В этом сообщении также указывается, как пользователь может исправить или ввести действительные данные.

Разрешение повторной попытки. При использовании этой стратегии на экран выводится понятное пользователю сообщение, объясняющее причину ошибки и предлагающее пользователю повторить операцию. Такая стратегия полезна, если ошибки формируются из-за возникновения временных исключительных ситуаций, таких как недоступность ресурса или истечение времени ожидания сети.

Вывод на экран универсальных сообщений. Если в приложении возникает непредвиденная ошибка, данные ошибки необходимо запротоколировать, но для пользователя вывести только универсальное сообщение. Предоставьте пользователю уникальный код ошибки, который может быть представлен группе технической поддержке. Эта стратегия полезна при возникновении непредвиденных исключений. Как правило, в случае возникновения непредвиденного исключения рекомендуется закрыть приложение, чтобы предотвратить повреждение данных или риски безопасности.

Более подробно методики обработки исключений обсуждаются в главе 17, «Сквозная функциональность». Enterprise Library, обеспечивающая полезные возможности для реализации стратегий обработки исключения, рассматривается в приложении F, «Enterprise

Library от patterns & practices».

Шаг 7 – Определение стратегии валидации

Эффективная стратегия валидации пользовательского ввода поможет фильтровать нежелательные или злонамеренные данные и будет способствовать повышению защищенность приложения. Как правило, валидация ввода осуществляется слоем представления, тогда как проверка на соответствие бизнес-правилам проводится компонентами бизнес-слоя. При проектировании стратегии проверки, прежде всего, необходимо определить все вводимые данные, подлежащие проверке. Например, ввод от Веб-клиента в поля формы, параметры (такие как данные операций GET и POST и строки запросов), скрытые поля и состояние представления (view state) подлежат проверке. В общем, проверяться должны все данные, поступающие из источников, не имеющих доверия.

Для приложений, имеющих компоненты и на стороне клиента, и на стороне сервера, таких как приложения RIA или насыщенные клиентские приложения, вызывающие сервисы на сервере приложений, кроме всех проверок на клиенте, должна проводиться дополнительная проверка на сервере. Но для повышения удобства использования и по соображениям производительности некоторые из проверок можно продублировать на клиенте. Проверка на клиенте позволяет обеспечивать пользователям быструю обратную связь в случаях ввода ими некорректных данных. Это может сохранить время и полосу пропускания, но не забывайте, что злоумышленники могут обойти любую реализованную на клиенте проверку.

Определившись с данными, подлежащими проверке, выберите методики проверки для них. Самыми распространенными методиками проверки являются:

Прием заведомо допустимого (Список разрешенного ввода или позитивная проверка). Принимаются только данные, удовлетворяющие заданным критериям, все остальные данные отклоняются.

Отклонение заведомо недопустимого (Список запрещенного ввода или негативная проверка). Принимаются данные, не содержащие известный набор символов или значений.

Очистка. Известные плохие символы или значения устраняются или преобразовываются с целью сделать ввод безопасным.

Вобщем, следует принимать заведомо допустимые значения (Список разрешенного ввода), а не пытаться выявить все возможные недействительные или злонамеренные значения, которые должны быть отклонены. Если невозможно полностью определить список известных допустимых значений, можно дополнить проверку частичным списком известных недопустимых значений и/или проводить очистку в качестве второй линии защиты.

Разные технологии представления используют разные подходы к проверке и информированию пользователя о проблемах. В WPF, к примеру, используются конвертеры и объекты правил проверки, часто подключаемые с помощью XAML, тогда как Windows Forms обеспечивает события проверки и привязки.

Более подробно методики валидации рассматриваются в главе 17, «Сквозная функциональность». Enterprise Library, обеспечивающая полезные возможности для проверки объектов и данных, как на стороне сервера, так и на стороне клиента, обсуждается в приложении F, «Enterprise Library от patterns & practices».

Предложения patterns & practices

Узнать о дополнительных предложениях группы Microsoft patterns & practices можно из следующих источников:

Composite Client Application Guidance for WPF1 (Руководство по проектированию составных клиентских приложений для WPF) как для настольных, так и для Silverlight-приложений; поможет в создании модульных приложений. Больше

1 Также известно как Prism (прим. научного редактора).

информации можно найти в статье «Composite Client Application Guidance» по адресу http://msdn.microsoft.com/en-us/library/cc707819.aspx.

Enterprise Library включает наборы блоков приложений, реализующих сквозную функциональность. Больше информации можно найти в статье «Enterprise Library»

по адресу http://msdn.microsoft.com/en-us/library/cc467894.aspx.

Software Factories (Фабрики ПО) ускоряют разработку определенных типов приложений, таких как смарт-клиенты, WPF-приложения и Веб-сервисы. Больше информации можно найти в статье «patterns & practices: by Application Type»

(patterns & practices: по типу приложения) по адресу http://msdn.microsoft.com/engb/practices/bb969054.aspx.

Unity Application Block (Блок Unity) как для корпоративных, так и для Silverlightсценариев; обеспечивает средства для реализации внедрения зависимостей, обнаружения сервисов и обращения управления. Больше информации можно найти в статье «Unity Application Block» по адресу http://msdn.microsoft.com/enus/library/dd203101.aspx.

Дополнительные источники

Электронная версия списка используемых источников доступна по адресу http://www.microsoft.com/architectureguide.

«Design Guidelines for Web Applications» (Руководство по проектированию Веб-

приложений) по адресу http://msdn.microsoft.com/en-us/library/ms978618.aspx.

«Data Binding Overview» (Обзор привязки данных) по адресу http://msdn.microsoft.com/en-us/library/ms752347.aspx.

«Design Guidelines for Exceptions» (Руководство по проектированию обработки исключений) по адресу http://msdn.microsoft.com/enus/library/ms229014%28VS.80%29.aspx.

Соседние файлы в папке ООП

  • #
  • #
  • #
  • #
  • #
  • #

Поскольку JavaScript сам по себе является динамическим языком и в течение многих лет не существовало фиксированных инструментов разработки, он обычно считается самым сложным языком программирования для отладки.

Когда возникает ошибка сценария, браузер обычно выдает сообщение, подобное «ожидаемому объекту» (отсутствующий объект), без контекстной информации, что сбивает с толку.

ECMAScript версии 3 посвящен решению этой проблемы, в частности, введены операторы try-catch и throw, а также некоторые типы ошибок, позволяющие разработчикам правильно обрабатывать ошибки.

Спустя несколько лет в веб-браузере появились отладчики и инструменты JavaScript. С 2008 года большинство веб-браузеров имеют возможность отлаживать код JavaScript.

Благодаря функциям языка и поддержке инструментов текущие разработчики смогли правильно реализовать обработку ошибок и найти источник ошибки.

1. Об ошибках, о которых сообщает браузер.

IE, Firefox, Safari, Chrome, Opera и другие основные браузеры имеют некоторый механизм для сообщения пользователям об ошибках JavaScript. По умолчанию все браузеры скрывают этот тип информации, ведь, кроме разработчиков, это мало кого волнует.
Поэтому при написании сценариев JavaScript на основе браузера не забудьте включить в браузере функцию отчетов JavaScript, чтобы получать своевременные уведомления об ошибках.

2. Обработка ошибок

Важность обработки ошибок при разработке программ неоспорима. Любое влиятельное веб-приложение требует комплексного механизма обработки ошибок.Конечно, это делают большинство лучших, но обычно это могут делать только серверные приложения.
На самом деле серверные группы обычно вкладывают много усилий в механизмы обработки ошибок и обычно рассматривают возможность классификации ошибок по типу, частоте или другим важным критериям. Таким образом, разработчики могут понять проблемы, которые могут возникнуть в приложении, когда пользователи используют простые запросы к базе данных или сценарии создания отчетов.
Хотя обработка ошибок клиентского приложения также важна, в последние годы ей все еще уделяется серьезное внимание. Фактически, мы должны признать неоспоримый факт: подавляющее большинство людей, пользующихся Интернетом, не являются техническими экспертами, а многие из них даже не знают
Что такое белый браузер, не говоря уже о том, чтобы они могли сказать, какой из них им нравится.
Как обсуждалось ранее в этой главе, поведение каждого браузера при возникновении ошибки JavaScript более или менее отличается. Некоторые будут отображать маленькие значки, а некоторые ничего не покажут.Поведение браузера по умолчанию при ошибках JavaScript не имеет правил для конечного пользователя. В идеальном случае пользователь сталкивается с ошибкой и не может понять, почему, он попытается сделать это снова; в худшем случае пользователь разозлится и уйдет навсегда.
Хороший механизм обработки ошибок позволяет пользователям вовремя напоминать о том, что произошло, чтобы они не паниковали. По этой причине, как разработчики, мы должны понимать, какие методы и инструменты доступны при работе с ошибками JavaScript.

2.1, инструкция try-catch

В ECMA-262 версии 3 оператор try-catch был представлен как стандартный способ обработки исключений в JavaScript. Базовый синтаксис показан ниже. Очевидно, что он в точности совпадает с оператором try-catch в Java.

try {
         // Код, который может вызвать ошибки
} catch(error) {
         // Что делать при возникновении ошибки
}

Другими словами, мы должны поместить весь код, который может вызывать ошибки, в блок try и поместить код обработки ошибок в блок catch. Например:

try {
    window.someNodexistentFunction();
} catch (error) {
    alert("An error happened!");
}

Если ошибка возникает в любом коде в блоке try, процесс выполнения кода немедленно завершается, а затем выполняется блок catch. На этом этапе блок catch получит объект, содержащий информацию об ошибке.
В отличие от других языков, даже если вы не хотите использовать этот объект ошибки, вы должны дать ему имя.
Фактическая информация, содержащаяся в этом объекте, будет варьироваться от браузера к браузеру, но обычно существует то, что есть свойство message, содержащее сообщение об ошибке.
ECMA-262 также указывает атрибут имени для сохранения типа ошибки; все текущие браузеры поддерживают этот атрибут (версии до Opera 9 не поддерживают этот атрибут). Поэтому при возникновении ошибки сообщение браузера может отображаться реалистично, как показано ниже.

try {
    window.someNodexistenFunction();
} catch (error) {
    alert(error.message);
}

В приведенном выше примере используется свойство message объекта ошибки при отображении сообщения об ошибке пользователю. Атрибут сообщения — единственный, который может гарантированно поддерживаться всеми браузерами.Кроме того, IE, Firefox, Safari, Chrome и Opera добавляют другую важную информацию к объекту события.
IE добавляет атрибут описания, который в точности совпадает с атрибутом сообщения, а также добавляет числовой атрибут, в котором хранится количество внутренних ошибок.
Firefox добавил атрибуты fileName, lineNumber и stack (включая информацию трассировки стека).
Safari добавил строку (представляющую номер строки), sourceId (представляющий код внутренней ошибки) и атрибуты sourceURL.
Конечно, при программировании в браузерах лучше использовать только атрибут сообщения.

1. наконец предложение

Хотя это необязательно в операторе try-catch, после использования предложения finally код все равно будет выполнен.
Другими словами, код в блоке try выполняется нормально, и будет выполнено предложение finally; если блок catch выполняется из-за ошибки, предложение finally все равно будет выполняться .
Пока код содержит предложение finally, независимо от того, какой код содержится в блоке инструкций try или catch — даже инструкции return, это не помешает выполнению предложения finally. Пример:

function testFinally () {
    try {
        return 2;
    } catch (error) {
        return 1;
    } finally {
        return 0;
    }
}

Вышеупомянутая функция помещает оператор возврата в каждую часть оператора try-catch. На первый взгляд, вызов этой функции вернет 2, потому что оператор return, который возвращает 2, находится в блоке try, и оператор выполняется без ошибок.
Однако, поскольку в конце есть предложение finally, оператор return будет проигнорирован в результате; то есть вызов этой функции может вернуть только 0. Если убрать предложение finally, эта функция вернет 2.
Если предоставляется предложение finally, предложение catch становится необязательным (достаточно либо catch, либо finally). В IE7 и более ранних версиях есть ошибка: если нет оператора catch, код в finally никогда не будет выполнен. Если вы все же хотите рассмотреть раннюю версию IE, вы должны предоставить предложение catch, даже если оно ничего не пишет. IE8 исправляет эту ошибку.

Помните, что до тех пор, пока код содержит предложение finally, оператор return в блоке try или catch будет игнорироваться. Поэтому, прежде чем использовать предложение finally, вы должны четко понимать, что вы хотите от кода.

  1. Тип ошибки

Есть много типов ошибок, которые могут возникнуть во время выполнения кода. Каждая ошибка имеет соответствующий тип ошибки, и при возникновении ошибки будет выдан объект ошибки соответствующего типа. ECMA-262 определяет следующие 7 типов ошибок:

  • Error
  • EvalError
  • RangeError
  • ReferenceError
  • SyntaxError
  • TypeError
  • URIError

Среди них Error является базовым типом, а другие типы ошибок наследуются от этого типа. Следовательно, все типы ошибок имеют один и тот же набор свойств (все методы в объекте ошибки являются методами объекта по умолчанию).
Ошибки типа ошибки редки, и если они есть, они также генерируются браузерами; основная цель этого базового типа заключается в том, чтобы разработчики выдавали настраиваемые ошибки.

Ошибки типа EvalError будут выданы при возникновении исключения с использованием функции eval (). Эта ошибка описана в ECMA-262 следующим образом: «Если значение атрибута eval используется в косвенном вызове (другими словами, его имя не используется явно в качестве идентификатора, то есть как MemberExpression в CallExpression), или присвоение атрибута Eval «. Проще говоря, если eval () не вызывается как функция, будет выдана ошибка, например:

new eval (); // выбросить EvalError
 eval = foo; // выбросить EvalError

На практике браузер не обязательно выдаст EvalError, когда должен выдать ошибку. Например, Firefox 4+ и IE8 выдаст TypeError для первого случая, а второй случай будет выполнен успешно без ошибок. Ввиду этого, в сочетании с тем фактом, что eval () редко используется в реальной разработке, возможность столкнуться с этим типом ошибки чрезвычайно мала.

Ошибки типа RangeError будут срабатывать, когда значение превышает соответствующий диапазон. Например, при определении массива, если вы укажете количество элементов, которые массив не поддерживает (например, -20 или Number.MAX_VALUE), эта ошибка будет вызвана. Ниже приводится конкретный пример.

var items1 = new Array (-20); // выбросить RangeError
 var items2 = new Array (Number.MAX_VALUE); // выбросить RangeError

Такая ошибка области видимости часто возникает в JavaScript.

В случае, если объект не может быть найден, возникнет ошибка ReferenceError (в этом случае это приведет непосредственно к хорошо известной ошибке браузера «ожидаемый объект»). Обычно эта ошибка возникает при доступе к несуществующей переменной, например:

var obj = x; // Выбрасываем ReferenceError, если x не объявлен

Что касается SyntaxError, когда мы передаем грамматически неверную строку JavaScript в функцию eval (), это вызовет этот тип ошибки. Например:

eval ("a ++ b"); // выбросить SyntaxError

Если код синтаксической ошибки появляется вне функции eval (), маловероятно использовать SyntaxError, потому что синтаксическая ошибка в это время приведет к немедленному прекращению выполнения кода JavaScript.

Тип TypeError часто используется в JavaScript. Эта ошибка может быть вызвана, когда неожиданный тип сохраняется в переменной или при доступе к методу, который не существует. Хотя есть много причин для ошибки, суть в том, что тип переменной не соответствует требованиям при выполнении операций, зависящих от типа. Давайте посмотрим на несколько примеров.

var o = new 10; // выбросить TypeError
 alert ("name" в true); // выбросить TypeError
 Function.prototype.toString.call ("name"); // выбросить TypeError

Наиболее распространенная ошибка типа возникает, когда параметры, переданные в функцию, не проверяются заранее, и в результате переданный тип не соответствует ожидаемому типу.

При использовании encodeURI () или decodeURI () и неправильном формате URI это вызовет ошибку URIError. Этот вид ошибок также встречается редко, потому что две упомянутые выше функции очень отказоустойчивы.

Используя различные типы ошибок, вы можете узнать больше об исключении, что поможет соответствующим образом исправить ошибку. Чтобы узнать тип ошибки, вы можете использовать оператор instanceof в операторе catch оператора try-catch следующим образом.

try {
    someFunction();
} catch (error){
    if (error instanceof TypeError){
                 // Обработка ошибок типа
    } else if (error instanceof ReferenceError){
                 // обрабатываем ссылочные ошибки
    } else {
                 // обрабатываем другие типы ошибок
    }
} 

В кроссбраузерном программировании проверка типа ошибки — самый простой способ определить метод обработки; сообщение об ошибке, содержащееся в свойстве сообщения, будет отличаться от браузера к браузеру.

3. Разумно используйте try-catch

Когда ошибка возникает в операторе try-catch, браузер будет думать, что ошибка была обработана, и поэтому не будет регистрировать или сообщать об ошибке с помощью механизма, описанного ранее в этой главе. Для тех веб-приложений, которые не требуют от пользователей понимания технологии или понимания ошибок, это должен быть идеальный результат.
Однако try-catch позволяет реализовать собственный механизм обработки ошибок. Использование try-catch лучше всего подходит для обработки ошибок, которые мы не можем контролировать. Предположим, вы используете функцию в большой библиотеке JavaScript. Функция может намеренно или непреднамеренно вызывать некоторые ошибки. Поскольку мы не можем изменять исходный код этой библиотеки, мы можем поместить вызов этой функции в оператор try-catch, и если возникнут какие-либо ошибки, мы сможем обработать их соответствующим образом.
Нецелесообразно использовать оператор try-catch, если вы четко знаете, что ваш код приведет к ошибке. Например, если параметр, переданный функции, является строкой, а не значением, это приведет к тому, что функция выдаст ошибку, тогда вам следует сначала проверить тип параметра, а затем решить, как это сделать. В этом случае не следует использовать оператор try-catch.

2.2, вывести ошибку

Существует также оператор throw, соответствующий оператору try-catch, который используется для выдачи пользовательских ошибок в любое время. При выдаче ошибки вы должны присвоить значение оператору throw. Тип этого значения не требуется. Все следующие коды действительны.

throw 12345;
throw "Hello world!";
throw true;
throw { name: "JavaScript"};

При обнаружении оператора throw код немедленно прекращает выполнение. Только когда оператор try-catch улавливает выброшенное значение, код продолжит выполнение.
Используя встроенный тип ошибки, ошибки браузера можно моделировать более реалистично. Конструктор для каждого типа ошибки получает один параметр, который является фактическим сообщением об ошибке. Ниже приведен пример.

throw new Error("Something bad happened.");

Эта строка кода вызывает общую ошибку с настраиваемым сообщением об ошибке. Браузер будет обрабатывать ошибки, вызванные этой строкой кода, как если бы это были ошибки, созданные им самим. Другими словами, браузер сообщит об этой ошибке обычным способом и отобразит здесь пользовательское сообщение об ошибке. Вы также можете имитировать похожие ошибки браузера, используя другие типы ошибок, как показано ниже.

throw new SyntaxError("I don’t like your syntax.");
throw new TypeError("What type of variable do you take me for?");
throw new RangeError("Sorry, you just don’t have the range.");
throw new EvalError("That doesn’t evaluate.");
throw new URIError("Uri, is that you?");
throw new ReferenceError("You didn’t cite your references properly.");

Наиболее часто используемые типы ошибок при создании настраиваемых сообщений об ошибках — это Error, RangeError, ReferenceError и TypeError.
Кроме того, цепочку прототипов можно также использовать для создания настраиваемых типов ошибок путем наследования Error. На этом этапе вам необходимо указать имя и атрибуты сообщения для вновь созданного типа ошибки. Давайте посмотрим на пример.

function CustomError(message){
    this.name = "CustomError";
    this.message = message;
} 

CustomError.prototype = new Error();

throw new CustomError("My message");

Браузер обрабатывает настраиваемые типы ошибок, унаследованные от Error, как и другие типы ошибок. Если вы хотите перехватывать собственные ошибки и обрабатывать их иначе, чем ошибки браузера, полезно создавать собственные ошибки.
IE будет отображать настраиваемое сообщение об ошибке только тогда, когда создается объект Error. Для других типов он без исключения отображает «выброшенное и не перехваченное исключение».

1. Когда выдавать ошибку

Чтобы получить больше информации о том, почему функция не может выполняться, очень удобным способом является выдача настраиваемой ошибки. Ошибка должна выдаваться при возникновении определенного известного состояния ошибки, которое приводит к сбою нормальной работы функции. Другими словами, браузер выдает ошибку при выполнении функции при определенных условиях. Например, следующая функция завершится ошибкой, если параметр не является массивом.

function process(values){
    values.sort();

    for (var i=0, len=values.length; i < len; i++){
        if (values[i] > 100){
            return values[i];
        }
    }
    return -1;
}

Если этой функции передается строковый параметр при ее выполнении, вызов sort () завершится ошибкой. В связи с этим разные браузеры будут выдавать разные сообщения об ошибках, но они не очень четкие, как показано ниже.

  • IE: атрибут или метод не существует.
  • Firefox: values.sort () не является функцией.
  • Safari: значение undefined (результат выражения values.sort) не является объектом.
  • Chrome: имя объекта не имеет метода сортировки.
  • Opera: Несоответствие типов (обычно используется не объектное значение, когда требуется объект).

Хотя Firefox, Chrome и Safari четко указали часть кода, вызвавшую ошибку, в сообщении об ошибке не было четко указано, что пошло не так и как это исправить.
При работе с функцией, аналогичной предыдущему примеру, нетрудно обработать эти сообщения об ошибках посредством отладки. Однако, столкнувшись со сложным веб-приложением, содержащим тысячи строк кода JavaScript, не так просто найти источник ошибки. В этом случае пользовательские ошибки с соответствующей информацией могут значительно улучшить ремонтопригодность кода. Взгляните на следующий пример.

function process(values){

    if (!(values instanceof Array)){
        throw new Error("process(): Argument must be an array.");
    }

    values.sort();

    for (var i=0, len=values.length; i < len; i++){
        if (values[i] > 100){
            return values[i];
        }
    }

    return -1;
}

В переписанной функции, если параметр values ​​не является массивом, будет выдана ошибка. Сообщение об ошибке содержит имя функции и четкое описание причины возникновения ошибки. Если эта ошибка возникает в сложном веб-приложении, гораздо проще найти источник проблемы.
Читателям рекомендуется в процессе разработки кода JavaScript обращать внимание на функции и факторы, которые могут вызвать сбой выполнения функции. Хороший механизм обработки ошибок должен гарантировать, что в коде будут возникать только те ошибки, которые вы сами бросаете.

2. Выкидывание ошибок и использование try-catch

Распространенный вопрос: когда выдавать ошибки, а когда использовать try-catch для их обнаружения. Вообще говоря, ошибки часто возникают на нижних уровнях архитектуры приложения, но этот уровень не влияет на текущий исполняемый код, поэтому ошибки обычно фактически не обрабатываются.
Если вы планируете написать библиотеку JavaScript, которая будет использоваться во многих приложениях, или даже просто написать вспомогательную функцию, которая может использоваться в нескольких местах в приложении, я настоятельно рекомендую вам предоставить подробная информация, когда выдается ошибка. Затем эти ошибки могут быть обнаружены в приложении и обработаны соответствующим образом.
Когда дело доходит до выдачи ошибок и их обнаружения, мы считаем, что вы должны перехватывать только те ошибки, которые вы точно знаете, как обрабатывать. Цель перехвата ошибок — предотвратить их обработку браузерами по умолчанию; цель выдачи ошибок — предоставить сообщение о конкретной причине ошибки.

2.3, ошибка (ошибка) событие

Любая ошибка, которая не обрабатывается try-catch, вызовет событие ошибки объекта окна. Это событие является одним из самых ранних событий, поддерживаемых веб-браузерами. Для обеспечения обратной совместимости IE, Firefox и Chrome не вносили никаких изменений в это событие (Opera и Safari не поддерживают событие ошибки).
В любом веб-браузере обработчик события onerror не создает объект события, но может получать три параметра: сообщение об ошибке, URL-адрес, по которому находится ошибка, и номер строки.
В большинстве случаев полезно только сообщение об ошибке, поскольку URL-адрес дает только местоположение документа, а строка кода, на которую указывает номер строки, может быть получена из встроенного кода JavaScript. или из внешнего исходного файла.
Чтобы указать обработчик события onerror, вы должны использовать технологию уровня DOM0, как показано ниже, которая не соответствует стандартному формату «событие уровня DOM2».

window.onerror = function(message, url, line){
    alert(message);
};

Пока возникает ошибка, независимо от того, генерируется она браузером или нет, событие ошибки будет инициировано и обработчик событий будет выполняться. Затем в игру вступает механизм браузера по умолчанию, который, как обычно, отображает сообщение об ошибке. Возврат false в обработчике событий, как показано ниже, может помешать поведению браузера по умолчанию сообщать об ошибках.

window.onerror = function(message, url, line){
    alert(message);
    return false;
};

Возвращая false, эта функция фактически действует как оператор try-catch во всем документе, который может перехватывать все ошибки времени выполнения без обработки кода. Этот обработчик событий является последней линией защиты, чтобы браузер не сообщал об ошибках, и в идеале его не следует использовать, когда это возможно. Пока оператор try-catch может использоваться надлежащим образом, в браузер не будет передаваться сообщение об ошибке, и событие ошибки не будет инициировано.
То, как браузеры используют это событие для обработки ошибок, существенно отличается. В IE, даже если произойдет событие ошибки, код все равно будет выполняться нормально; все переменные и данные будут сохранены, поэтому к ним можно будет получить доступ в обработчике события onerror.
Но в Firefox обычный код перестанет выполняться, и все, что было до события
Некоторые переменные и данные будут уничтожены, поэтому определить ошибку практически невозможно.
Изображение также поддерживает событие ошибки. Пока URL-адрес в функции src изображения не может возвращать распознаваемый формат изображения, будет инициировано событие ошибки. Событие ошибки в это время следует формату DOM и возвращает объект события, нацеленный на изображение. Ниже приведен пример.

var image = new Image();

EventUtil.addHandler(image, "load", function(event){
    alert("Image loaded!");
});

EventUtil.addHandler(image, "error", function(event){
    alert("Image not loaded!");
});

 image.src = "smilex.gif"; // Указываем несуществующий файл

В этом примере отображается окно с предупреждением, когда изображение не загружается. Следует отметить, что при возникновении события ошибки процесс загрузки изображения завершается, а это означает, что его нельзя загрузить повторно.

2.4 Стратегии обработки ошибок

В прошлом так называемая стратегия обработки ошибок веб-приложений ограничивалась серверной частью. Говоря об ошибках и обработке ошибок, обычно следует учитывать множество аспектов, включая некоторые инструменты, такие как системы регистрации и мониторинга. Эти инструменты предназначены для анализа шаблонов ошибок, отслеживания причины ошибки и помощи в определении того, на скольких пользователей она повлияет.
На стороне JavaScript веб-приложения стратегии обработки ошибок не менее важны. Поскольку любая ошибка JavaScript может сделать страницу непригодной для использования, важно знать, когда и почему произошла ошибка. Подавляющее большинство пользователей веб-приложений технически невежественны и легко расстраиваются, когда сталкиваются с ошибками. Иногда они могут обновить страницу, чтобы решить проблему, а иногда отказываются от своих усилий. Как разработчик вы должны знать, когда код может пойти не так, что не так, а также иметь систему для отслеживания таких проблем.

2.5, распространенные типы ошибок

Суть обработки ошибок — это сначала знать, какие ошибки будут возникать в коде. Поскольку JavaScript имеет слабую типизацию и не проверяет параметры функции, ошибки будут возникать только во время выполнения кода. Вообще говоря, есть три типа ошибок, на которые следует обратить внимание:

  • Ошибка преобразования типа
  • Ошибка типа данных
  • Ошибка связи

Вышеуказанные ошибки будут возникать в определенном режиме или при недостаточной проверке значения.

1. Ошибка преобразования типа

Ошибки преобразования типов возникают при использовании определенного оператора или других языковых структур, которые могут автоматически преобразовывать значения.
При использовании операторов равенства (==) и неравенства (! =) или использовании не-логических значений в операторах управления потоком, таких как if, for и while, преобразования типов происходят чаще всего. часто ошибка.
Операторы равенства и неравенства преобразуют различные типы значений перед выполнением сравнения. Поскольку в нединамических языках разработчики используют одни и те же символы для выполнения интуитивно понятных сравнений, они часто используются неправильно таким же образом в JavaScript.
В большинстве случаев мы рекомендуем использовать конгруэнтные (===) и неконгруэнтные (! ==) операторы, чтобы избежать преобразования типов. Давайте посмотрим на пример.

alert(5 == "5"); // true
alert(5 === "5"); // false
alert(1 == true); // true
alert(1 === true); // false

Здесь используются операторы равенства и сравнения для сравнения значения 5 со строкой «5». Оператор равенства сначала преобразует значение 5 в строку «5», а затем сравнивает его с другой строкой «5», и результат является истинным. Оператор сравнения знает, что сравниваются два разных типа данных, поэтому он напрямую возвращает false. То же верно и для 1: оператор равенства считает их равными, а оператор сравнения — нет.
Использование конгруэнтных и неконгруэнтных операторов позволяет избежать ошибок преобразования типов, вызванных использованием операторов равенства и неравенства, поэтому мы настоятельно рекомендуем их использовать.

Еще одно место, где могут возникать ошибки преобразования типов, — операторы управления потоком. Такие операторы, как if, автоматически преобразуют любое значение в логическое перед определением следующей операции. Особенно, если операторы при неправильном использовании наиболее подвержены ошибкам. Взгляните на следующий пример.

function concat(str1, str2, str3){
    var result = str1 + str2;
         if (str3) {// Никогда этого не делайте !!!
        result += str3;
    }
    return result;
}

Цель этой функции — объединить две или три строки, а затем вернуть результат. Среди них третья строка является необязательной, поэтому ее необходимо проверить.
Неиспользуемым именованным переменным автоматически будет присвоено значение undefined. Неопределенное значение может быть преобразовано в логическое значение false, поэтому оператор if в этой функции фактически применяется только к случаю, когда предоставляется третий параметр. Проблема в том, что не только undefined может быть преобразовано в false, и не только строковые значения могут быть преобразованы в true.
Например, если третий параметр имеет значение 0, проверка оператора if завершится неудачно, а проверка значения 1 пройдет.

Использование не-логических значений в операторах управления потоком данных — очень частый источник ошибок. Чтобы избежать таких ошибок, необходимо фактически передавать логические значения при сравнении условий. Фактически, этого можно достичь, выполнив некоторую форму сравнения. Например, мы можем переписать предыдущую функцию следующим образом.

function concat(str1, str2, str3){
    var result = str1 + str2;
    if (typeof str3 == "string") {// подходящее сравнение
        result += str3;
    }
    return result;
}

В этой переписанной функции условие оператора if возвращает логическое значение на основе сравнения. Эта функция относительно более надежна, и на нее нелегко повлиять аномальные значения.

2. Ошибка типа данных

JavaScript имеет слабую типизацию, то есть перед использованием переменных и параметров функций они не будут сравниваться, чтобы убедиться в правильности их типов данных. Чтобы гарантировать, что ошибки типа данных не возникнут, разработчики могут полагаться только на разработку соответствующего кода обнаружения типа данных.
В случае передачи в функцию неожиданных значений вероятнее всего возникнут ошибки типа данных.
В предыдущем примере можно проверить третий параметр, чтобы убедиться, что это строка, но два других параметра не проверяются. Если функция должна возвращать строку, просто передайте ей два значения и проигнорируйте третий параметр, что может легко привести к неверному результату выполнения. Аналогичная ситуация существует и в следующей функции.

// Небезопасная функция, любое нестроковое значение вызовет ошибку
function getQueryString(url){
    var pos = url.indexOf("?");
    if (pos > -1){
        return url.substring(pos +1);
    }
    return "";
}

Цель этой функции — вернуть строку запроса по заданному URL-адресу. Для этого сначала используется indexOf (), чтобы найти знак вопроса в строке. Если найдено, используйте метод substring (), чтобы вернуть все строки после вопросительного знака.
Две функции в приведенном выше примере могут управлять только строками, поэтому простая передача других типов данных вызовет ошибки. И добавьте простой оператор определения типа, вы можете
Убедитесь, что функция не подвержена ошибкам.

function getQueryString(url){
         if (typeof url == "string") {// Обеспечиваем безопасность, проверяя тип
        var pos = url.indexOf("?");
        if (pos > -1){
            return url.substring(pos +1);
        }
    }
    return "";
}

Переписанная функция сначала проверяет, является ли переданное значение строкой. Таким образом гарантируется, что функция не вызовет ошибок из-за получения нестроковых значений.

Как упоминалось в предыдущем разделе, использование небулевых значений в качестве условий в операторах управления потоком может легко привести к ошибкам преобразования типов. Точно так же это часто приводит к ошибкам типа данных. Взгляните на следующий пример.

// Небезопасная функция, любое значение, не являющееся массивом, вызовет ошибку
function reverseSort(values){
         if (values) {// Никогда не делайте этого !!!
        values.sort();
        values.reverse();
    }
}

Эта функция reverseSort () может отменить сортировку массива, которая использует методы sort () и reverse ().
Для условия управления в операторе if любое значение, не являющееся массивом, которое будет преобразовано в true, вызовет ошибку. Другая распространенная ошибка — сравнение параметров с нулевыми значениями, как показано ниже.

// Небезопасная функция, любое значение, не являющееся массивом, вызовет ошибку
function reverseSort(values){
         if (values! = null) {// Никогда этого не делайте !!!
        values.sort();
        values.reverse();
    }
}

Сравнение с нулевым значением может гарантировать только то, что соответствующие значения не являются нулевыми и неопределенными (это эквивалентно использованию операций равенства и неравенства). Чтобы гарантировать, что переданное значение является допустимым, недостаточно обнаружить нулевое значение; поэтому этот метод не следует использовать. Точно так же мы не рекомендуем сравнивать значение с undefined.
Еще один неправильный подход — выполнять обнаружение функции только для определенной функции, которая будет использоваться. Взгляните на следующий пример.

// Все еще небезопасно, любое значение, не являющееся массивом, вызовет ошибку
function reverseSort(values){
         if (typeof values.sort == "function") {// Никогда этого не делайте !!!
        values.sort();
        values.reverse();
    }
}

В этом примере код сначала проверяет, существует ли метод sort () в параметре. Таким образом, если вы передадите объект (а не массив), содержащий метод sort (), он обязательно пройдет проверку, но вы можете получить ошибку при вызове функции reverse ().
Если вы точно знаете, какой тип следует передать, лучше всего использовать instanceof для определения его типа данных, как показано ниже.

// безопасные значения без массива будут проигнорированы
function reverseSort(values){
         if (values ​​instanceof Array) {// проблема решена
        values.sort();
        values.reverse();
    }
}

Последняя функция reverseSort () безопасна: она проверяет значения, чтобы убедиться, что этот параметр является экземпляром типа Array. Таким образом вы можете гарантировать, что функция игнорирует любые значения, не являющиеся массивами.

Вообще говоря, значение базового типа следует проверять с помощью typeof, а значение объекта — с помощью instanceof.
В зависимости от того, как используется функция, иногда нет необходимости проверять типы данных всех параметров один за другим. Однако общедоступные API должны безоговорочно выполнять проверку типов, чтобы гарантировать, что функции всегда могут выполняться нормально.

3. Ошибка связи

С развитием программирования Ajax для веб-приложений стало обычным делом динамически загружать информацию или функции в течение их жизненного цикла. Однако любое взаимодействие между JavaScript и сервером может вызвать ошибки.

Первый тип ошибок связи связан с неверно отформатированным URL-адресом или отправленными данными. Наиболее частая проблема заключается в том, что данные не кодируются с помощью encodeURIComponent () перед отправкой на сервер. Например, формат следующего URL-адреса неверен:

http://www.yourdomain.com/?redir=http://www.someotherdomain.com?a=b&c=d

Вызовите encodeURIComponent () для всех строк после «redir =», чтобы решить эту проблему, и результатом будет следующая строка:

http://www.yourdomain.com/?redir=http%3A%2F%2Fwww.someotherdomain.com%3Fa%3Db%26c%3Dd

Для строк запроса вы должны помнить, что вы должны использовать метод encodeURIComponent (). Чтобы гарантировать это, иногда вы можете определить функцию для обработки строки запроса, например:

function addQueryStringArg(url, name, value){
    if (url.indexOf("?") == -1){
        url += "?";
    } else {
        url += "&";
    }

    url += encodeURIComponent(name) + "=" + encodeURIComponent(value);
    return url;
}

Эта функция получает три параметра: URL-адрес, добавляемый к строке запроса, имя параметра и значение параметра. Если входящий URL-адрес не содержит вопросительного знака, добавьте к нему вопросительный знак; в противном случае добавьте амперсанд, потому что вопросительный знак означает, что есть другие строки запроса. Затем добавьте к URL-адресу имя и значение закодированной строки запроса. Вы можете использовать эту функцию следующим образом:

var url = "http://www.somedomain.com";
var newUrl = addQueryStringArg(url, "redir", "http://www.someotherdomain.com?a=b&c=d");

alert(newUrl);

Использование этой функции вместо создания URL-адресов вручную может обеспечить правильную кодировку и избежать связанных с ней ошибок.

Кроме того, когда сервер отвечает неверными данными, могут возникнуть ошибки связи.
Используя две технологии сценария динамической загрузки и динамический стиль загрузки, можно столкнуться с недоступностью ресурсов. Без возврата соответствующих ресурсов Firefox, Chrome и Safari выйдут из строя, а IE и Opera будут сообщать об ошибках.
Однако трудно судить и устранять ошибки, вызванные использованием этих двух технологий. В некоторых случаях использование связи Ajax может предоставить дополнительную информацию о статусе ошибки.

2.6, различать фатальные ошибки и нефатальные ошибки

Самая важная часть любой стратегии обработки ошибок — определить, является ли ошибка фатальной. Для нефатальных ошибок это может быть определено на основе одного или нескольких из следующих условий:

  • Не влияет на основную задачу пользователя;
  • Влияет только на часть страницы;
  • Можно восстановить
  • Повторение той же операции может устранить ошибку.

По сути, нефатальные ошибки не вызывают беспокойства.
Например, Yahoo! Mail (http://mail.yahoo.com) Есть функция, которая позволяет пользователям отправлять текстовые сообщения мобильного телефона через его интерфейс. Если по какой-то причине вы не можете отправлять текстовые сообщения, это не фатальная ошибка, потому что это не основная функция приложения. Пользователи используют Yahoo! Mail в основном для проверки и написания электронных писем. Только когда эта основная функция работает нормально, нет причин прерывать пользователя.
Нет необходимости запрашивать пользователя, потому что произошла нефатальная ошибка — вы можете заменить затронутую область на странице, например, сообщением о том, что соответствующая функция не может быть используемый. Однако, если пользователь прерывается из-за этого, в этом нет необходимости.

Неустранимые ошибки могут быть определены одним или несколькими из следующих условий:

  • Приложение вообще не может продолжать работу;
  • Ошибка явно повлияла на основную операцию пользователя;
  • Вызовет другие побочные ошибки.

Чтобы принять соответствующие меры, вы должны знать, при каких обстоятельствах возникает фатальная ошибка в JavaScript. В случае фатальной ошибки вы должны немедленно отправить пользователю сообщение о том, что он больше не может продолжать то, что делает. Если страницу необходимо обновить, чтобы приложение работало в обычном режиме, пользователь должен быть уведомлен, и в то же время пользователю должен быть предоставлен щелчок для обновления страницы.

Основная основа для различения нефатальных ошибок и фатальных ошибок — это их влияние на пользователей. Хорошо спроектированный код может гарантировать, что ошибка в одной части приложения не повлияет без необходимости на другую часть, которая фактически не имеет значения.
Например, My Yahoo! (http://my.yahoo.comПерсонализированная домашняя страница содержит множество независимых модулей. Если каждый модуль необходимо инициализировать с помощью вызовов JavaScript, вы можете увидеть код, подобный следующему:

for (var i=0, len=mods.length; i < len; i++){
         mods [i] .init (); // может вызвать фатальные ошибки
}

На первый взгляд, в этих кодах нет ничего плохого: вызовите метод init () для каждого модуля по очереди. Проблема в том, что любая ошибка в методе init () любого модуля приведет к тому, что все последующие модули в массиве больше не будут инициализированы.
С логической точки зрения писать подобный код бессмысленно. В конце концов, каждый модуль не зависит друг от друга и каждый реализует разные функции. Вероятная причина фатальных ошибок — структура кода. Однако с помощью следующей модификации все ошибки модуля можно сделать нефатальными:

for (var i=0, len=mods.length; i < len; i++){
    try {
        mods[i].init();
    } catch (ex) {
                 // здесь обрабатываем ошибки
    }
}

При добавлении оператора try-catch в цикл for любая ошибка инициализации любого модуля не повлияет на инициализацию других модулей.
В переписанном выше коде при возникновении ошибки соответствующая ошибка будет обработана независимо и не повлияет на работу пользователя.

2.7, запишите ошибку на сервер

Обычной практикой в ​​процессе разработки веб-приложений является централизованное сохранение журналов ошибок, чтобы найти причину важных ошибок.
Например, ошибки базы данных и сервера регулярно записываются в журналы и классифицируются в соответствии с обычно используемыми API.
В сложных веб-приложениях мы также рекомендуем вам записывать ошибки JavaScript на сервер. Другими словами, эти ошибки также должны быть записаны в то место, где хранятся ошибки на стороне сервера, но они должны быть отмечены с передней стороны. Сбор внешних и внутренних ошибок может значительно облегчить анализ данных.
Чтобы создать такую ​​систему записи ошибок JavaScript, сначала необходимо создать страницу (или точку входа на сервер) на сервере для обработки данных об ошибках. Функция этой страницы — не что иное, как получение данных из строки запроса, а затем запись данных в журнал ошибок. Эта страница может использовать следующие функции:

function logError(sev, msg){
    var img = new Image();
    img.src = "log.php?sev=" + encodeURIComponent(sev) + "&msg=" + encodeURIComponent(msg);
}

Функция logError () принимает два параметра: числовое значение или строку (в зависимости от используемой системы), представляющую серьезность и сообщение об ошибке. Среди них объект Image используется для отправки запроса, который очень гибкий, в основном в следующих аспектах.

  • Все браузеры поддерживают объект Image, включая те, которые не поддерживают объект XMLHttpRequest.
  • Можно избежать междоменных ограничений. Обычно за обработку ошибок с нескольких серверов отвечает один сервер, и в этом случае использования XMLHttpRequest недостаточно.
  • Вероятность возникновения проблем в процессе записи ошибок относительно невысока. Большая часть взаимодействия Ajax осуществляется с помощью функций оболочки, предоставляемых библиотекой JavaScript.Если есть проблема с самим кодом библиотеки, и вы все еще полагаетесь на библиотеку для регистрации ошибок, возможно, сообщения об ошибках не могут быть записаны.

Пока используется оператор try-catch, соответствующая ошибка должна записываться в журнал. Взгляните на следующий пример.

for (var i=0, len=mods.length; i < len; i++){
    try {
        mods[i].init();
    } catch (ex){
        logError("nonfatal", "Module init failed: " + ex.message);
    }
}

Здесь в случае сбоя инициализации модуля будет вызвана функция logError (). Первый параметр — «нефатальный», который указывает серьезность ошибки. Второй параметр — это контекстная информация плюс реальное сообщение об ошибке JavaScript. Сообщения об ошибках, записываемые на сервере, должны содержать как можно больше контекстной информации, чтобы определить настоящую причину ошибки.

3. Технология отладки

В эпоху, когда было не так просто найти отладчики JavaScript, разработчикам приходилось использовать свой творческий потенциал, чтобы отлаживать свой код различными методами. В результате появилась практика размещения кода так или иначе для вывода отладочной информации.
Среди них наиболее распространенный способ — вставить функцию alert () повсюду в отлаживаемом коде. Но этот подход, с одной стороны, более проблематичен (требуется очистка после отладки), с другой стороны, он может вызвать новые проблемы (представьте себе результат оставления функции alert () в коде продукта). В настоящее время существует множество лучших инструментов отладки, поэтому мы больше не рекомендуем использовать alert () при отладке.

3.1, выводить сообщения в консоль

IE8, Firefox, Opera, Chrome и Safari имеют консоли JavaScript, которые можно использовать для просмотра ошибок JavaScript. Более того, в этих браузерах вы можете выводить сообщения на консоль через код.
Для Firefox необходимо установить Firebug (www.getfirebug.com), потому что Firefox использует консоль Firebug.
Для IE8, Firefox, Chrome и Safari вы можете писать сообщения в консоль JavaScript через объект console, который имеет следующие методы.

  • error (message): записывать сообщения об ошибках в консоль
  • info (message): записывать информационные сообщения в консоль
  • log (сообщение): записывать общие сообщения в консоль
  • warn (message): записывать предупреждающие сообщения в консоль

В IE8, Firebug, Chrome и Safari методы, используемые для записи сообщений, разные, и сообщения об ошибках, отображаемые в консоли, разные. Сообщения об ошибках имеют красный значок, а предупреждающие сообщения — желтый значок. Следующая функция показывает пример использования консоли для вывода сообщений.

function sum(num1, num2){
    console.log("Entering sum(), arguments are " + num1 + "," + num2);

    console.log("Before calculation");
    var result = num1 + num2;
    console.log("After calculation");

    console.log("Exiting sum()");
    return result;
}

При вызове этой функции sum () в консоли появятся некоторые сообщения, которые можно использовать для облегчения отладки.

В версиях Opera до 10.5 доступ к консоли JavaScript можно получить с помощью метода opera.postError (). Этот метод принимает один параметр — параметр, который должен быть записан в консоль, и используется следующим образом.

function sum(num1, num2){
    opera.postError("Entering sum(), arguments are " + num1 + "," + num2);

    opera.postError("Before calculation");
    var result = num1 + num2;
    opera.postError("After calculation");

    opera.postError("Exiting sum()");
    return result;
}

Не смотрите на имя метода opera.postError (). Кажется, что он может только выводить ошибки, но на самом деле он может записывать любую информацию в консоль JavaScript.

Другое решение — использовать LiveConnect, то есть запускать код Java на JavaScript. Firefox, Safari и Opera поддерживают LiveConnect, поэтому вы можете работать с консолью Java. Например, следующий код может записывать сообщения в консоль Java на JavaScript.

java.lang.System.out.println("Your message"); 

Вы можете использовать эту строку кода для замены console.log () или opera.postError (), как показано ниже.

function sum(num1, num2){
    java.lang.System.out.println("Entering sum(), arguments are " + num1 + "," + num2);

    java.lang.System.out.println("Before calculation");
    var result = num1 + num2;
    java.lang.System.out.println("After calculation");

    java.lang.System.out.println("Exiting sum()");
    return result;
}

Если система настроена правильно, консоль Java может отображаться сразу после вызова LiveConnect.

В браузерах нет механизма для записи сообщений в консоль JavaScript, но следующие функции можно использовать как единый интерфейс.

function log(message){
    if (typeof console == "object"){
        console.log(message);
    } else if (typeof opera == "object"){
        opera.postError(message);
    } else if (typeof java == "object" && typeof java.lang == "object"){
        java.lang.System.out.println(message);
    }
}

Эта функция log () определяет, какой интерфейс консоли JavaScript доступен, а затем использует соответствующий интерфейс. Эту функцию можно безопасно использовать в любом браузере, не вызывая ошибок, например:

function sum(num1, num2){
    log("Entering sum(), arguments are " + num1 + "," + num2);

    log("Before calculation");
    var result = num1 + num2;
    log("After calculation");

    log("Exiting sum()");
    return result;
}

Запись сообщений в консоль JavaScript может помочь в отладке кода, но при публикации приложения вы также должны удалить все сообщения.
При развертывании приложения очистку можно выполнить вручную или с помощью определенных шагов обработки кода.

Ведение журнала сообщений предпочтительнее использования функции alert (), потому что окно предупреждения блокирует выполнение программы, а при определении влияния асинхронной обработки на время использование окна предупреждения повлияет на результат.

3.2. Записать сообщение на текущую страницу

Другой способ вывода отладочных сообщений — открыть небольшую область на странице для отображения сообщений. Эта область обычно является элементом, и этот элемент может всегда отображаться на странице, но только для целей отладки; он также может быть элементом, динамически создаваемым по мере необходимости. Например, функцию log () можно изменить следующим образом:

function log(message){
    var console = document.getElementById("debuginfo");
    if (console === null){
        console = document.createElement("div");
        console.id = "debuginfo";
        console.style.background = "#dedede";
        console.style.border = "1px solid silver";
        console.style.padding = "5px";
        console.style.width = "400px";
        console.style.position = "absolute";
        console.style.right = "0px";
        console.style.top = "0px";
        document.body.appendChild(console);
    }
    console.innerHTML += "<p>" + message + "</p>";
}

Эта модифицированная функция log () сначала определяет, есть ли отладочный элемент, если нет, она создает новый элемент <div> и применяет к нему некоторые стили, чтобы отличить его от других элементов на странице.
Затем используйте innerHTML, чтобы записать сообщение в этот элемент <div>. В результате на странице появляется небольшая область, на которой отображается сообщение об ошибке. Этот метод очень полезен в IE7 и более ранних версиях или других браузерах, которые не поддерживают консоль JavaScript.
Подобно записи сообщений об ошибках в консоль, код, который выводит сообщения об ошибках на страницу, также следует удалить перед публикацией.

3.3, вывести ошибку

Как упоминалось ранее, выдача ошибок также является хорошим способом отладки кода. Если сообщение об ошибке очень конкретное, его можно использовать в качестве основы для определения источника ошибки. Но такое сообщение об ошибке должно четко указывать причину ошибки, чтобы сохранить другие операции отладки. Рассмотрим следующую функцию:

function divide(num1, num2){
    return num1 / num2;
}

Эта простая функция вычисляет деление двух чисел, но если один из параметров не является числом, она вернет NaN. Если такое простое вычисление вернет NaN, это вызовет проблемы в веб-приложении. В связи с этим перед расчетом вы можете проверить, является ли каждый параметр числовым значением. Например:

function divide(num1, num2){
    if (typeof num1 != "number" || typeof num2 != "number") {
        throw new Error("divide(): Both arguments must be numbers.");
    }
    return num1 / num2;
} 

Здесь, если параметр не является значением, будет выдана ошибка. Сообщение об ошибке содержит название функции и реальную причину ошибки. Пока браузер сообщает об этом сообщении об ошибке, мы можем сразу узнать источник ошибки и характер проблемы.
Условно говоря, это конкретное сообщение об ошибке более полезно, чем общее сообщение об ошибке браузера.

Для больших приложений пользовательские ошибки обычно генерируются с помощью функции assert (). Эта функция принимает два параметра: один — это условие, что результат оценки должен быть истинным, а другой — ошибка, которая будет выдана, если условие ложно. Ниже приводится очень простая функция assert ().

function assert(condition, message){
    if (!condition){
        throw new Error(message);
    }
}

Вы можете использовать эту функцию assert () для замены операторов if в некоторых функциях, которые необходимо отлаживать для вывода сообщений об ошибках. Ниже приведен пример использования этой функции.

function divide(num1, num2){
    assert(typeof num1 == "number" && typeof num2 == "number", "divide(): Both arguments must be numbers.");

    return num1 / num2;
}

Видно, что использование функции assert () может уменьшить объем кода, необходимого для выдачи ошибки, и его легче понять, чем предыдущий код.

4. Распространенные ошибки IE

В течение многих лет IE был самым сложным браузером для отладки ошибок JavaScript. Сообщения об ошибках, выдаваемые IE, обычно короткие и нечеткие, а контекстной информации очень мало или вообще отсутствует.
Но как браузер с наибольшим количеством пользователей, то, как понять ошибки, выдаваемые IE, также вызывает наибольшую озабоченность. В следующих разделах будут обсуждаться некоторые ошибки JavaScript, которые сложно отладить в IE.

4.1, прекращение работы

В версиях до IE8 существует ошибка, которая является наиболее запутанной, раздражающей и трудной для отладки по сравнению с другими браузерами: операция прервана.
При изменении страницы, которая еще не была загружена, произойдет ошибка завершения операции. При возникновении ошибки появляется модальное диалоговое окно с сообщением «Операция прекращена». Нажмите кнопку «ОК», чтобы выгрузить всю страницу, а затем отобразите пустой экран; отладка в настоящее время очень сложна. Следующий пример вызовет ошибку завершения операции.

<!DOCTYPE html>
<html>
<head>
    <title>Operation Aborted Example</title>
</head>
<body>
    <p>The following code should cause an Operation Aborted error in IE versions
 prior to 8.</p> 
    <div>
        <script type="text/javascript">
            document.body.appendChild(document.createElement("div"));
        </script>
    </div>
</body>
</html>

Проблема в приведенном выше примере заключается в том, что код JavaScript должен изменить document.body до загрузки страницы, а элемент <script> не является прямым потомком элемента <body>.
Чтобы быть точным, когда узел <script> содержится в элементе, а код JavaScript должен использовать appendChild (), innerHTML или другие методы DOM для изменения родительского элемента или предка element Когда элемент выбран, произойдет ошибка завершения операции (потому что только элементы, которые были загружены, могут быть изменены).
Чтобы избежать этой проблемы, вы можете дождаться загрузки целевого элемента, прежде чем использовать его, или использовать другие методы работы. Например, добавление наложения, которое абсолютно позиционируется на странице для document.body, является очень распространенной операцией.
Обычно разработчики используют метод appendChild () для добавления этого элемента, но легко переключиться на метод insertBefore (). Следовательно, если вы измените одну строку кода в предыдущем примере, вы можете избежать ошибки завершения операции.

<!DOCTYPE html>
<html>
<head>
    <title>Operation Aborted Example</title>
</head>
<body>
    <p>The following code should not cause an Operation Aborted error in IE
 versions prior to 8.</p>
    <div>
        <script type="text/javascript">
            document.body.insertBefore(document.createElement("div"),
            document.body.firstChild);
        </script>
    </div>
</body>
</html>

В этом примере новый элемент <div> добавляется в начало document.body вместо конца. Поскольку вся информация, необходимая для выполнения этой операции, известна при запуске сценария, это не вызовет ошибки.

Помимо изменения метода, вы также можете переместить элемент <script> из содержащего элемента и напрямую использовать его как дочерний элемент <body>. Например:

<!DOCTYPE html>
<html>
<head>
    <title>Operation Aborted Example</title>
</head>
<body>
    <p>The following code should not cause an Operation Aborted error in IE
 versions prior to 8.</p>
    <div>
    </div>
    <script type="text/javascript">
        document.body.appendChild(document.createElement("div"));
    </script> 
</body>
</html> 

На этот раз ошибки не произойдет, потому что скрипт изменяет свой прямой родительский элемент, а не косвенный родительский элемент. В той же ситуации IE8 больше не выдает ошибку завершения операции, а вместо этого выдает обычную ошибку JavaScript со следующим сообщением об ошибке:

HTML Parsing Error: Unable to modify the parent container element 
before the child element is closed (KB927917).

Однако, хотя ошибки, создаваемые браузерами, различны, решение остается тем же.

4.2, недопустимые символы

Согласно синтаксису, файл JavaScript должен содержать только определенные символы. Если в файле JavaScript есть недопустимые символы, IE выдаст ошибку недопустимого символа.
Так называемые недопустимые символы — это символы, не определенные в синтаксисе JavaScript. Например, есть символ, который выглядит как знак минус, но представлен значением Unicode 8211 ( u2013), который нельзя использовать как обычный знак минуса (код ASCII — 45), потому что этот символ не определен в JavaScript. синтаксис.
Этот символ обычно автоматически вставляется в документы Word. Если ваш код скопирован из документа Word в текстовый редактор, а затем запущен в IE, вы можете столкнуться с ошибками с недопустимыми символами.
Другие браузеры реагируют на недопустимые символы аналогично IE. Firefox выдаст ошибку недопустимого символа, Safari сообщит о синтаксической ошибке, а Opera сообщит об этом. ReferenceError (ошибка ссылки), потому что она интерпретирует недопустимые символы как неопределенные идентификаторы.

4.3, участников не найдено

Как упоминалось ранее, все объекты DOM в IE реализованы в форме COM-объектов, а не собственных объектов JavaScript. Это может привести к очень странному поведению, связанному со сборкой мусора.
Ошибка «Элемент не найден» в IE напрямую вызвана неправильной координацией процедуры сборки мусора.
В частности, если вы присваиваете значение объекту после уничтожения объекта, это вызовет ошибку «член не найден». Причем причиной этой ошибки должен быть COM-объект.
Чаще всего эта ошибка возникает при использовании объекта события. Объект события в IE является свойством окна. Этот объект создается при возникновении события и уничтожается после выполнения последнего обработчика события. Предположим, вы используете объект события в закрытии, и закрытие не будет выполнено немедленно, а затем вызов его в будущем и присвоение значения атрибуту события приведет к ошибке «член не найден», как показано в следующем примере.

document.onclick = function(){
    var event = window.event;
    setTimeout(function(){
                 event.returnValue = false; // Участник не найден ошибка
    }, 1000);
};

В этом коде мы назначаем документу обработчик события щелчка. В обработчике событий window.event сохраняется в переменной события.
Затем закрытие, переданное в setTimeout (), содержит переменную события. Когда обработчик события щелчка выполняется, объект события будет уничтожен, а члены объекта, на который имеется ссылка в замыкании, больше не будут существовать. Другими словами, поскольку COM-объекту нельзя присвоить значение после уничтожения его члена, присвоение returnValue в закрытии приведет к ошибке «член не найден».

4.4. Неизвестная ошибка времени выполнения

При использовании innerHTML или outerHTML для указания HTML следующими способами возникнет неизвестная ошибка времени выполнения: первая — вставка блочного элемента во встроенный элемент, а вторая — доступ к любой части таблицы (<table>, <tbody > И т.д.). Например, с технической точки зрения тег <span> не может содержать элементы уровня блока, такие как <div>, поэтому следующий код вызовет неизвестную ошибку времени выполнения:

span.innerHTML = "<div> Hi </div>"; // Здесь span содержит элемент <div>

В случае вставки блочного элемента в неподходящее место другие браузеры попытаются исправить и скрыть ошибку, и IE на этом этапе вполне прав.

4.5, синтаксическая ошибка

Обычно, как только IE сообщает о синтаксической ошибке (синтаксической ошибке), причину ошибки можно быстро найти. На данный момент причина может заключаться в том, что в коде отсутствует точка с запятой или фигурные скобки не соответствуют друг другу. Однако есть и другая ситуация, когда причина не очень очевидна и требует особого внимания.
Если вы ссылаетесь на внешний файл JavaScript, а в конце файл не возвращает код JavaScript, IE также выдаст синтаксическую ошибку.
Например, атрибут src элемента <script> указывает на HTML-файл, что вызовет синтаксическую ошибку. Сообщая о местонахождении синтаксической ошибки, обычно говорят, что ошибка находится в первом символе первой строки сценария.
Opera и Safari также будут сообщать об ошибках синтаксиса, но они предоставят информацию о внешнем файле, вызвавшем проблему; IE не предоставит эту информацию, поэтому нам нужно дважды проверить ссылку самостоятельно. Файл JavaScript.
Но Firefox будет игнорировать ошибки синтаксического анализа в файлах, отличных от JavaScript, которые встроены в документ как содержимое JavaScript.

Этот вид ошибки более вероятен, когда серверный компонент динамически генерирует JavaScript. Многие серверные языки вставляют HTML-код в вывод при возникновении ошибки времени выполнения, и такой вывод, содержащий HTML, может легко нарушить синтаксис JavaScript. Если у вас возникли проблемы с отслеживанием синтаксических ошибок, мы рекомендуем вам дважды проверить указанные внешние файлы, чтобы убедиться, что эти файлы не содержат HTML, вставленный сервером из-за ошибок.

4.6. Система не может найти указанный ресурс

Заявление «Система не может найти указанный ресурс» (система не может найти указанный ресурс), вероятно, является наиболее ценным сообщением об ошибке, выдаваемым IE.
использует JavaScript для запроса URL-адреса ресурса, и длина URL-адреса превышает URL-адрес IE
Эта ошибка возникает, если не может превышать ограничение в 2083 символа. IE не только ограничивает длину URL-адреса, используемого в JavaScript, но также ограничивает длину URL-адреса, используемого пользователем в самом браузере (другие браузеры не имеют таких строгих ограничений на URL-адреса).
IE также имеет ограничение в 2048 символов для путей URL. Следующий код вызовет ошибку.

function createLongUrl(url){
    var s = "?";
    for (var i=0, len=2500; i < len; i++){
        s += "a";
    }

    return url + s;
}

var x = new XMLHttpRequest(); 
x.open("get", createLongUrl("http://www.somedomain.com/"), true);
x.send(null);

В приведенном выше примере объект XMLHttpRequest пытается отправить запрос на URL-адрес, превышающий максимальный предел длины. При вызове метода open () возникает ошибка. Способ избежать этой проблемы — сократить длину строки запроса, задав более короткие имена параметрам строки запроса или сократив ненужные данные.
Кроме того, вы также можете изменить метод запроса на POST и отправлять данные через тело запроса вместо строки запроса.

резюме

Обработка ошибок критически важна для современной разработки сложных веб-приложений. Неспособность заранее предсказать возможные ошибки и неспособность принять заранее стратегии восстановления может привести к неудовлетворительному опыту работы и, в конечном итоге, к неудовлетворенности пользователей.
Большинство браузеров по умолчанию не сообщают пользователям об ошибках, поэтому вам необходимо включить функцию отчетов об ошибках в браузере во время разработки и отладки. Однако в введенном в эксплуатацию коде продукта больше не должно быть таких отчетов об ошибках.

Вот несколько способов, чтобы браузер не реагировал на ошибки JavaScript.

  • Используйте операторы try-catch там, где могут возникать ошибки, чтобы у вас была возможность реагировать на ошибки соответствующим образом, не используя механизм обработки ошибок браузера.
  • Используйте обработчик событий window.onerror, этот метод может принимать все ошибки, которые не может обработать try-catch (только для IE, Firefox и Chrome).

Кроме того, любое веб-приложение должно анализировать возможные источники ошибок и разрабатывать план их устранения.

  • Прежде всего, должно быть ясно, что такое фатальная ошибка, а какая нефатальная.
  • Во-вторых, проанализируйте код, чтобы определить наиболее вероятную ошибку. Основные причины ошибок в JavaScript следующие.
    • Преобразование типов
    • Недостаточное определение типов данных
    • Ошибка в данных, отправленных или полученных с сервера

IE, Firefox, Chrome, Opera и Safari имеют отладчики JavaScript, некоторые из них являются встроенными, а некоторые представляют собой расширения, которые необходимо загрузить. Все эти отладчики поддерживают установку точек останова, контроль выполнения кода и проверку значений переменных во время выполнения.

Disclaimer: I do not know any theory on error-handling, I did, however, thought repetitively about the subject as I explored various languages and programming paradigms, as well as toyed around with programming language designs (and discussed them). What follows, thus, is a summary of my experience so far; with objective arguments.

Note: this should cover all the questions, but I did not even try to address them in order, preferring instead a structured presentation. At the end of each section, I present a succinct answer to those questions it answered, for clarity.


Introduction

As a premise, I would like to note that whatever is subject to discussion some parameters must be kept in mind when designing a library (or reusable code).

The author cannot hope to fathom how this library will be used, and should thus avoid strategies that make integration more difficult than it should. The most glaring defect would be relying on globally shared state; thread-local shared state can also be a nightmare for interactions with coroutines/green-threads. The use of such coroutines and threads also highlight that synchronization best be left to the user, in single-threaded code it will mean none (best performance), whilst in coroutines and green-threads the user is best suited to implement (or use existing implementations of) dedicated synchronization mechanisms.

That being said, when library are for internal use only, global or thread-local variables might be convenient; if used, they should be clearly documented as a technical limitation.


Logging

There are many ways to log messages:

  • with extra information such as timestamp, process-ID, thread-ID, server name/IP, …
  • via synchronous calls or with an asynchronous mechanism (and an overflow handling mechanism)
  • in files, databases, distributed databases, dedicated log-servers, …

As the author of a library, the logs should be integrated within the client infrastructure (or turned off). This is best provided by allowing the client to provide hooks so as to deal with the logs themselves, my recommendation is:

  • to provide 2 hooks: one to decide whether to log or not, and one to actually log (the message being formatted and the latter hook called only when the client decided to log)
  • to provide, on top of the message: a severity (aka level), the filename, line and function name if open-source or otherwise the logical module (if several)
  • to, by default, write to stdout and stderr (depending on severity), until the client explicitly says not to log

I would note that, following the guidelines delineated in the introduction, synchronization is left to the client.

Regarding whether to log errors: do not log (as errors) what you otherwise already report via your API; you can however still log at a lower severity the details. The client can decide whether to report or not when handling the error, and for example choose not to report it if this was just a speculative call.

Note: some information should not make it into the logs and some other pieces are best obfuscated. For example, passwords should not be logged, and Credit-Card or Passport/Social Security Numbers are best obfuscated (partly at least). In a library designed for such sensitive information, this can be done during logging; otherwise the application should take care of this.

Is logging something that should only be done in application code? Or is it ok to do some logging from library code.

Application code should decide the policy. Whether a library logs or not depends on whether it needs to.


Going on after an error ?

Before we actually talk about reporting errors, the first question we should ask is whether the error should be reported (for handling) or if things are so wrong that aborting the current process is clearly the best policy.

This is certainly a tricky topic. In general, I would advise to design such that going on is an option, with a purge/reset if necessary. If this cannot be achieved in certain cases, then those cases should provoke an abortion of the process.

Note: on some systems, it is possible to get a memory-dump of the process. If an application handles sensitive data (password, credit-cards, passports, …), it is best deactivated in production (but can be used during development).

Note: it can be interesting to have a debug switch that transforms a portion of the error-reporting calls into abortions with a memory-dump to assist debugging during development.


Reporting an error

The occurrence of an error signifies that the contract of a function/interface could not be fulfilled. This has several consequences:

  • the client should be warned, which is why the error should be reported
  • no partially correct data should escape in the wild

The latter point will be treated later on; for now let us focus on reporting the error. The client should not, ever, be able to accidentally ignore this report. Which is why using error-codes is such an abomination (in languages when return values can be ignored):

ErrorStatus_t doit(Input const* input, Output* output);

I know of two schemes that require explicit action on the client part:

  • exceptions
  • result types (optional<T>, either<T, U>, …)

The former is well-known, the latter is very much used in functional languages and was introduced in C++11 under the guise of std::future<T> though other implementations exist.

I advise to prefer the latter, when possible, as it easier to fathom, but revert to exceptions when no result is expected. Contrast:

Option<Value&> find(Key const&);

void updateName(Client::Id id, Client::Name name);

In the case of «write-only» operations such as updateName, the client has no use for a result. It could be introduced, but it would be easy to forget the check.

Reverting to exceptions also occur when a result type is impractical, or insufficient to convey the details:

Option<Value&> compute(RepositoryInterface&, Details...);

In such a case of externally defined callback, there is an almost infinite list of potential failures. The implementation could use the network, a database, the filesystem, … in this case, and in order to report errors accurately:

  • the externally defined callback should be expected to report errors via exceptions when the interface is insufficient (or impractical) to convey the full details of the error.
  • the functions based on this abstract callback should be transparent to those exceptions (let them pass, unmodified)

The goal is to let this exception bubble up to the layer where the implementation of the interface was decided (at least), for it’s only at this level that there is a chance to correctly interpret the exception thrown.

Note: the externally defined callback is not forced to use exceptions, we should just expect it might be using some.


Using an error

In order to use an error report, the client need enough information to take a decision. Structured information, such as error codes or exception types, should be preferred (for automatic actions) and additional information (message, stack, …) can be provided in a non-structured way (for humans to investigate).

It would be best if a function clearly documented all possible failure modes: when they occur and how they are reported. However, especially in case arbitrary code is executed, the client should be prepared to deal with unknown codes/exceptions.

A notable exception is, of course, result types: boost::variant<Output, Error0, Error1, ...> provides a compiler-checked exhaustive list of known failure modes… though a function returning this type could still throw, of course.

How to decide between logging an error, or showing it as an error message to the user?

The user should always be warned when its order could not be fulfilled, however a user-friendly (understandable) message should be displayed. If possible, advices or work-arounds should be presented as well. Details are for investigating teams.


Recovering from an error ?

Last, but certainly not least, comes the truly frightening part about errors: recovery.

This is something that databases (real ones) are so good for: transaction-like semantics. If anything unexpected occurs, the transaction is aborted as if nothing had happened.

In the real world, things are not simple. The simple example of cancelling an e-mail sent pops to mind: too late. Protocols may exist, depending on your application domain, but this is out of this discussion. The first step, though, is the ability to recover a sane in-memory state; and that is far from being simple in most languages (and STM can only do so much today).

First of all, an illustration of the challenge:

void update(Client& client, Client::Name name, Client::Address address) {
    client.update(std::move(name));
    client.update(std::move(address)); // Throws
}

Now, after updating the address failed, I am left with a half-updated client. What can I do ?

  • attempting to undo all the updates that occurred is close to impossible (the undo might fail)
  • copying the state prior to executing any single update is a performance hog (supposing we can even swap it back in a sure way)

In any case, the book-keeping required is such that mistakes will creep in.

And worst of all: there is no safe assumption that can be made as to the extent of the corruption (except that client is now botched). Or at least, no assumption that will endure time (and code changes).

As often, the only way to win is not to play.


A possible solution: Transactions

Wherever possible, the key idea is to define macro functions, that will either fail or produce the expected result. Those are our transactions. And their form is invariant:

Either<Output, Error> doit(Input const&);

// or

Output doit(Input const&); // throw in case of error

A transaction does not modify any external state, thus if it fails to produce a result:

  • the external world has not changed (nothing to rollback)
  • there is no partial result to observe

Any function that is not a transaction should be considered as having corrupted anything it touched, and thus the only sane way of dealing with an error from non-transactional functions is to let it bubble up until a transaction layer is reached. Any attempt to deal with the error prior is, in the end, doomed to fail.

How to decide if an error should be handled locally or propagated to higher level code ?

In case of exceptions, where should you generally catch them? In low-level or higher level code?

Deal with them whenever it is safe to do so and there is value in doing so. Most notably, it’s okay to catch an error, check if it can be dealt with locally, and then either deal with it or pass it up.


Should you strive for a unified error handling strategy through all layers of code, or try to develop a system that can adapt itself to a variety of error handling strategies (in order to be able to deal with errors from 3rd party libraries).

I did not address this question previously, however I believe it is clear than the approach I highlighted is already dual since it consists of both result-types and exceptions. As such, dealing with 3rd party libraries should be a cinch, though I do advise wrapping them anyway for other reasons (3rd party code is better insulated beyond a business-oriented interface tasked with the impedance adaption).

Disclaimer: I do not know any theory on error-handling, I did, however, thought repetitively about the subject as I explored various languages and programming paradigms, as well as toyed around with programming language designs (and discussed them). What follows, thus, is a summary of my experience so far; with objective arguments.

Note: this should cover all the questions, but I did not even try to address them in order, preferring instead a structured presentation. At the end of each section, I present a succinct answer to those questions it answered, for clarity.


Introduction

As a premise, I would like to note that whatever is subject to discussion some parameters must be kept in mind when designing a library (or reusable code).

The author cannot hope to fathom how this library will be used, and should thus avoid strategies that make integration more difficult than it should. The most glaring defect would be relying on globally shared state; thread-local shared state can also be a nightmare for interactions with coroutines/green-threads. The use of such coroutines and threads also highlight that synchronization best be left to the user, in single-threaded code it will mean none (best performance), whilst in coroutines and green-threads the user is best suited to implement (or use existing implementations of) dedicated synchronization mechanisms.

That being said, when library are for internal use only, global or thread-local variables might be convenient; if used, they should be clearly documented as a technical limitation.


Logging

There are many ways to log messages:

  • with extra information such as timestamp, process-ID, thread-ID, server name/IP, …
  • via synchronous calls or with an asynchronous mechanism (and an overflow handling mechanism)
  • in files, databases, distributed databases, dedicated log-servers, …

As the author of a library, the logs should be integrated within the client infrastructure (or turned off). This is best provided by allowing the client to provide hooks so as to deal with the logs themselves, my recommendation is:

  • to provide 2 hooks: one to decide whether to log or not, and one to actually log (the message being formatted and the latter hook called only when the client decided to log)
  • to provide, on top of the message: a severity (aka level), the filename, line and function name if open-source or otherwise the logical module (if several)
  • to, by default, write to stdout and stderr (depending on severity), until the client explicitly says not to log

I would note that, following the guidelines delineated in the introduction, synchronization is left to the client.

Regarding whether to log errors: do not log (as errors) what you otherwise already report via your API; you can however still log at a lower severity the details. The client can decide whether to report or not when handling the error, and for example choose not to report it if this was just a speculative call.

Note: some information should not make it into the logs and some other pieces are best obfuscated. For example, passwords should not be logged, and Credit-Card or Passport/Social Security Numbers are best obfuscated (partly at least). In a library designed for such sensitive information, this can be done during logging; otherwise the application should take care of this.

Is logging something that should only be done in application code? Or is it ok to do some logging from library code.

Application code should decide the policy. Whether a library logs or not depends on whether it needs to.


Going on after an error ?

Before we actually talk about reporting errors, the first question we should ask is whether the error should be reported (for handling) or if things are so wrong that aborting the current process is clearly the best policy.

This is certainly a tricky topic. In general, I would advise to design such that going on is an option, with a purge/reset if necessary. If this cannot be achieved in certain cases, then those cases should provoke an abortion of the process.

Note: on some systems, it is possible to get a memory-dump of the process. If an application handles sensitive data (password, credit-cards, passports, …), it is best deactivated in production (but can be used during development).

Note: it can be interesting to have a debug switch that transforms a portion of the error-reporting calls into abortions with a memory-dump to assist debugging during development.


Reporting an error

The occurrence of an error signifies that the contract of a function/interface could not be fulfilled. This has several consequences:

  • the client should be warned, which is why the error should be reported
  • no partially correct data should escape in the wild

The latter point will be treated later on; for now let us focus on reporting the error. The client should not, ever, be able to accidentally ignore this report. Which is why using error-codes is such an abomination (in languages when return values can be ignored):

ErrorStatus_t doit(Input const* input, Output* output);

I know of two schemes that require explicit action on the client part:

  • exceptions
  • result types (optional<T>, either<T, U>, …)

The former is well-known, the latter is very much used in functional languages and was introduced in C++11 under the guise of std::future<T> though other implementations exist.

I advise to prefer the latter, when possible, as it easier to fathom, but revert to exceptions when no result is expected. Contrast:

Option<Value&> find(Key const&);

void updateName(Client::Id id, Client::Name name);

In the case of «write-only» operations such as updateName, the client has no use for a result. It could be introduced, but it would be easy to forget the check.

Reverting to exceptions also occur when a result type is impractical, or insufficient to convey the details:

Option<Value&> compute(RepositoryInterface&, Details...);

In such a case of externally defined callback, there is an almost infinite list of potential failures. The implementation could use the network, a database, the filesystem, … in this case, and in order to report errors accurately:

  • the externally defined callback should be expected to report errors via exceptions when the interface is insufficient (or impractical) to convey the full details of the error.
  • the functions based on this abstract callback should be transparent to those exceptions (let them pass, unmodified)

The goal is to let this exception bubble up to the layer where the implementation of the interface was decided (at least), for it’s only at this level that there is a chance to correctly interpret the exception thrown.

Note: the externally defined callback is not forced to use exceptions, we should just expect it might be using some.


Using an error

In order to use an error report, the client need enough information to take a decision. Structured information, such as error codes or exception types, should be preferred (for automatic actions) and additional information (message, stack, …) can be provided in a non-structured way (for humans to investigate).

It would be best if a function clearly documented all possible failure modes: when they occur and how they are reported. However, especially in case arbitrary code is executed, the client should be prepared to deal with unknown codes/exceptions.

A notable exception is, of course, result types: boost::variant<Output, Error0, Error1, ...> provides a compiler-checked exhaustive list of known failure modes… though a function returning this type could still throw, of course.

How to decide between logging an error, or showing it as an error message to the user?

The user should always be warned when its order could not be fulfilled, however a user-friendly (understandable) message should be displayed. If possible, advices or work-arounds should be presented as well. Details are for investigating teams.


Recovering from an error ?

Last, but certainly not least, comes the truly frightening part about errors: recovery.

This is something that databases (real ones) are so good for: transaction-like semantics. If anything unexpected occurs, the transaction is aborted as if nothing had happened.

In the real world, things are not simple. The simple example of cancelling an e-mail sent pops to mind: too late. Protocols may exist, depending on your application domain, but this is out of this discussion. The first step, though, is the ability to recover a sane in-memory state; and that is far from being simple in most languages (and STM can only do so much today).

First of all, an illustration of the challenge:

void update(Client& client, Client::Name name, Client::Address address) {
    client.update(std::move(name));
    client.update(std::move(address)); // Throws
}

Now, after updating the address failed, I am left with a half-updated client. What can I do ?

  • attempting to undo all the updates that occurred is close to impossible (the undo might fail)
  • copying the state prior to executing any single update is a performance hog (supposing we can even swap it back in a sure way)

In any case, the book-keeping required is such that mistakes will creep in.

And worst of all: there is no safe assumption that can be made as to the extent of the corruption (except that client is now botched). Or at least, no assumption that will endure time (and code changes).

As often, the only way to win is not to play.


A possible solution: Transactions

Wherever possible, the key idea is to define macro functions, that will either fail or produce the expected result. Those are our transactions. And their form is invariant:

Either<Output, Error> doit(Input const&);

// or

Output doit(Input const&); // throw in case of error

A transaction does not modify any external state, thus if it fails to produce a result:

  • the external world has not changed (nothing to rollback)
  • there is no partial result to observe

Any function that is not a transaction should be considered as having corrupted anything it touched, and thus the only sane way of dealing with an error from non-transactional functions is to let it bubble up until a transaction layer is reached. Any attempt to deal with the error prior is, in the end, doomed to fail.

How to decide if an error should be handled locally or propagated to higher level code ?

In case of exceptions, where should you generally catch them? In low-level or higher level code?

Deal with them whenever it is safe to do so and there is value in doing so. Most notably, it’s okay to catch an error, check if it can be dealt with locally, and then either deal with it or pass it up.


Should you strive for a unified error handling strategy through all layers of code, or try to develop a system that can adapt itself to a variety of error handling strategies (in order to be able to deal with errors from 3rd party libraries).

I did not address this question previously, however I believe it is clear than the approach I highlighted is already dual since it consists of both result-types and exceptions. As such, dealing with 3rd party libraries should be a cinch, though I do advise wrapping them anyway for other reasons (3rd party code is better insulated beyond a business-oriented interface tasked with the impedance adaption).

Понравилась статья? Поделить с друзьями:
  • Стратегическая ошибка наполеона 1812
  • Страсть к графомании лексическая ошибка
  • Стратегии природы как мудрость леса изменит нашу жизнь эрвин тома скачать
  • Странный парадокс лексическая ошибка или нет
  • Странники искали счастья у всех встречных женщин стилистическая ошибка