ОБОЙТИ ОШИБКИ В КОДЕ

Как обнаружить ошибку

Прочитай информацию об исключении

Если выполнение программы прерывается исключением, то это первое место откуда стоит начинать поиск. 

В каждом языке есть свои способы уведомления об исключениях. Например в JavaScript для обработки ошибок связанных с Web Api существует DOMException

. Для пользовательских сценариев есть базовый тип Error

. В обоих случаях в них содержится информация о наименовании и описании ошибки.

Для . NET существует класс Exception

и каждое исключение в приложении унаследовано от данного класса, который представляет ошибки происходящие во время выполнения программы. В свойстве Message читаем текст ошибки. Это даёт общее понимание происходящего. В свойстве Source смотрим в каком объекте произошла ошибка. В InnerException смотрим, нет ли внутреннего исключения и если было, то разворачиваем его и смотрим информацию уже в нём. В свойстве StackTrace хранится строковое представление информации о стеке вызова в момент появления ошибки.

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

Всю полученную информацию читаем вдумчиво и внимательно. Любая деталь важна при поиске ошибки. Иногда начинающие разработчики не придают значения этому описанию. Например в . NET при возникновении ошибки NRE с описанием параметра, который разработчик задаёт выше по коду. Из-за этого думает, что параметр не может быть NRE, а значит ошибка в другом месте. На деле оказывается, что ошибки транслируют ту картину, которую видит среда выполнения и первым делом за гипотезу стоит взять утверждение, что этот параметр равен null. Поэтому разберитесь при каких условиях параметр стал null, даже если он определялся выше по коду.

Пример неявного переопределения параметров – использование интерцептора, который изменяет этот параметр в запросе и о котором вы не знаете.

Разверните стек

Когда выбрасывается исключение, помимо самого описания ошибки полезно изучить стек выполнения. Для . NET его можно посмотреть в свойстве исключения StackTrace. Для JavaScript аналогично смотрим в Error.prototype.stack (свойство не входит в стандарт) или можно вывести в консоль выполнив console.trace(). В стеке выводятся названия методов в том порядке в котором они вызывались. Если то место, где падает ошибка зависит от аргументов которые пришли из вызывающего метода, то если развернуть стек, мы проследим где эти аргументы формировались.

Загуглите текст ошибки

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

Прочитайте документацию

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

Проведите исследовательское тестирование

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

Бинарный поиск

В неочевидных случаях, если нет уверенности что проблема в вашем коде, а сообщение об ошибке не даёт понимания где проблема,  комментируем блок кода в котором обнаружилась проблема. Убеждаемся что ошибка пропала. Аналогично бинарному алгоритму раскомментировали половину кода, проверили воспроизводимость ошибки. Если воспроизвелась, закомментировали половину выполняемого кода, повторили проверку и так далее пока не будет локализовано место появления ошибки.

Объект ошибки

Когда возникает ошибка, JavaScript генерирует объект, содержащий её детали. Затем этот объект передаётся как аргумент в блок catch
:

   try {
  // ...
} catch(err) { // <-- объект ошибки, можно использовать другое название вместо err
  // ...
}  
  

Для всех встроенных ошибок этот объект имеет два основных свойства:

name
Имя ошибки. Например, для неопределённой переменной это "ReferenceError"
.
message
Текстовое сообщение о деталях ошибки.

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

stack
Текущий стек вызова: строка, содержащая информацию о последовательности вложенных вызовов, которые привели к ошибке. Используется в целях отладки.
   try {
  lalala; // ошибка, переменная не определена!
} catch(err) {
  alert(err.name); // ReferenceError
  alert(err.message); // lalala is not defined
  alert(err.stack); // ReferenceError: lalala is not defined at (...стек вызовов)

  // Можем также просто вывести ошибку целиком
  // Ошибка приводится к строке вида "name: message"
  alert(err); // ReferenceError: lalala is not defined
}  
  

Генерация собственных ошибок

Что если json
синтаксически корректен, но не содержит необходимого свойства name
?

   let json = '{ "age": 30 }'; // данные неполны

try {

  let user = JSON.parse(json); // <-- выполнится без ошибок
  alert( user.name ); // нет свойства name!

} catch (e) {
  alert( "не выполнится" );
}  
  

Здесь JSON.parse
выполнится без ошибок, но на самом деле отсутствие свойства name
для нас ошибка.

Для того, чтобы унифицировать обработку ошибок, мы воспользуемся оператором throw
.

Оператор «throw»

Оператор throw
генерирует ошибку.

Технически в качестве объекта ошибки можно передать что угодно. Это может быть даже примитив, число или строка, но всё же лучше, чтобы это был объект, желательно со свойствами name
и message
(для совместимости со встроенными ошибками).

В JavaScript есть множество встроенных конструкторов для стандартных ошибок: Error
, SyntaxError
, ReferenceError
, TypeError
и другие. Можно использовать и их для создания объектов ошибки.

   let error = new Error(message);
// или
let error = new SyntaxError(message);
let error = new ReferenceError(message);
// ...  
  

Для встроенных ошибок (не для любых объектов, только для ошибок), свойство name
– это в точности имя конструктора. А свойство message
берётся из аргумента.

   let error = new Error("Ого, ошибка! o_O");

alert(error.name); // Error
alert(error.message); // Ого, ошибка! o_O  
  

Давайте посмотрим, какую ошибку генерирует JSON.parse
:

   try {
  JSON.parse("{ некорректный json o_O }");
} catch(e) {
  alert(e.name); // SyntaxError
  alert(e.message); // Unexpected token b in JSON at position 2
}  
  

Как мы видим, это SyntaxError
.

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

   let json = '{ "age": 30 }'; // данные неполны

try {

  let user = JSON.parse(json); // <-- выполнится без ошибок

  if (!user.name) {
    throw new SyntaxError("Данные неполны: нет имени"); // (*)
  }

  alert( user.name );

} catch(e) {
  alert( "JSON Error: " + e.message ); // JSON Error: Данные неполны: нет имени
}  
  

В строке (*)
оператор throw
генерирует ошибку SyntaxError
с сообщением message
. Точно такого же вида, как генерирует сам JavaScript. Выполнение блока try
немедленно останавливается, и поток управления прыгает в catch
.

Теперь блок catch
становится единственным местом для обработки всех ошибок: и для JSON.parse
и для других случаев.

Оборачивание исключений

И, для полноты картины – последняя, самая продвинутая техника по работе с ошибками. Она, впрочем, является стандартной практикой во многих объектно-ориентированных языках.

Цель функции readData
в примере выше – прочитать данные. При чтении могут возникать разные ошибки, не только SyntaxError
, но и, возможно, к примеру URIError
(неправильное применение функций работы с URI) да и другие.

Код, который вызвал readData
, хотел бы иметь либо результат, либо информацию об ошибке.

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

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

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

Мы его видим везде в грамотно построенном коде, но не всегда отдаём себе в этом отчёт.

В данном случае, если при чтении данных происходит ошибка, то мы будем генерировать её в виде объекта ReadError
, с соответствующим сообщением. А «исходную» ошибку на всякий случай тоже сохраним, присвоим в свойство cause
(англ. – причина).

Выглядит это так:

   function ReadError(message, cause) {
  this.message = message;
  this.cause = cause;
  this.name = 'ReadError';
  this.stack = cause.stack;
}

function readData() {
  var data = '{ bad data }';

  try {
    // ...
    JSON.parse(data);
    // ...
  } catch (e) {
    // ...
    if (e.name == 'URIError') {
      throw new ReadError("Ошибка в URI", e);
    } else if (e.name == 'SyntaxError') {
      throw new ReadError("Синтаксическая ошибка в данных", e);
    } else {
      throw e; // пробрасываем
    }
  }
}

try {
  readData();
} catch (e) {
  if (e.name == 'ReadError') {
    alert( e.message );
    alert( e.cause ); // оригинальная ошибка-причина
  } else {
    throw e;
  }
}  
  

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

Читайте также:  СКАНЕР КОДОВ ОШИБОК CAN

Работа с ошибками на клиенте

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

▍Сохранение сведений об ошибках в состоянии приложения

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

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

  1. Глобальные ошибки — в эту категорию попадают сообщения об ошибках общего характера, приходящие с сервера, или ошибки, которые, например, возникают в том случае, если пользователь не вошёл в систему и в других подобных ситуациях.
  2. Специфические ошибки, выдаваемые серверной частью приложения — сюда относятся ошибки, сведения о которых приходят с сервера. Например, подобная ошибка возникает, если пользователь попытался войти в систему и отправил на сервер имя и пароль, а сервер сообщил ему о том, что пароль неправильный. Подобные вещи в клиентской части приложения не проверяются, поэтому сообщения о таких ошибках должны приходить с сервера.
  3. Специфические ошибки, выдаваемые клиентской частью приложения. Пример такой ошибки — сообщение о некорректном адресе электронной почты, введённом в соответствующее поле.

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

Здесь будет использоваться встроенная в React система управления состоянием приложения, но, при необходимости, вы можете воспользоваться и специализированными решениями для управления состоянием — такими, как MobX или Redux.

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

ОБОЙТИ ОШИБКИ В КОДЕ

Сообщение о глобальной ошибке

Теперь взглянем на код, который хранится в файле Application.js
.

   import React, { Component } from 'react'

import GlobalError from './GlobalError'

class Application extends Component {
    constructor(props) {
        super(props)

        this.state = {
            error: '',
        }

        this._resetError = this._resetError.bind(this)
        this._setError = this._setError.bind(this)
    }

    render() {
        return (
            <div className="container">
                <GlobalError error={this.state.error} resetError={this._resetError} />
                <h1>Handling Errors</h1>
            </div>
        )
    }

    _resetError() {
        this.setState({ error: '' })
    }

    _setError(newError) {
        this.setState({ error: newError })
    }
}

export default Application  
  

Как видно, в состоянии, в Application.js
, имеется место для хранения данных ошибки. Кроме того, тут предусмотрены методы для сброса этих данных и для их изменения.

Ошибка и метод для сброса ошибки передаётся компоненту GlobalError
, который отвечает за вывод сообщения об ошибке на экран и за сброс ошибки после нажатия на значок x
в поле, где выводится сообщение. Вот код компонента GlobalError
(файл GlobalError.js
).

   import React, { Component } from 'react'

class GlobalError extends Component {
    render() {
        if (!this.props.error) return null

        return (
            <div
                style={{
                    position: 'fixed',
                    top: 0,
                    left: '50%',
                    transform: 'translateX(-50%)',
                    padding: 10,
                    backgroundColor: '#ffcccc',
                    boxShadow: '0 3px 25px -10px rgba(0,0,0,0.5)',
                    display: 'flex',
                    alignItems: 'center',
                }}
            >
                {this.props.error}
                 
                <i
                    className="material-icons"
                    style={{ cursor: 'pointer' }}
                    onClick={this.props.resetError}
                >
                    close
                </font></i>
            </div>
        )
    }
}

export default GlobalError  
  

Обратите внимание на строку if (!this.props.error) return null
. Она указывает на то, что при отсутствии ошибки компонент ничего не выводит. Это предотвращает постоянный показ красного прямоугольника на странице. Конечно, вы, при желании, можете поменять внешний вид и поведение этого компонента. Например, вместо того, чтобы сбрасывать ошибку по нажатию на x
, можно задать тайм-аут в пару секунд, по истечении которого состояние ошибки сбрасывается автоматически.

Теперь, когда всё готово для работы с глобальными ошибками, для задания глобальной ошибки достаточно воспользоваться _setError
из Application.js
. Например, это можно сделать в том случае, если сервер, после обращения к нему, вернул сообщение об общей ошибке ( error: 'GENERIC'
). Рассмотрим пример (файл GenericErrorReq.js
).

   import React, { Component } from 'react'
import axios from 'axios'

class GenericErrorReq extends Component {
    constructor(props) {
        super(props)

        this._callBackend = this._callBackend.bind(this)
    }

    render() {
        return (
            <div>
                <button onClick={this._callBackend}>Click me to call the backend</button>
            </div>
        )
    }

    _callBackend() {
        axios
            .post('/api/city')
            .then(result => {
                // сделать что-нибудь с результатом в том случае, если запрос оказался успешным
            })
            .catch(err => {
                if (err.response.data.error === 'GENERIC') {
                    this.props.setError(err.response.data.description)
                }
            })
    }
}

export default GenericErrorReq  
  

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

▍Обработка специфических ошибок, возникающих при выполнении запросов

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

ОБОЙТИ ОШИБКИ В КОДЕ

Сообщение о специфической ошибке

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

   import React, { Component } from 'react'
import axios from 'axios'

import InlineError from './InlineError'

class SpecificErrorRequest extends Component {
    constructor(props) {
        super(props)

        this.state = {
            error: '',
        }

        this._callBackend = this._callBackend.bind(this)
    }

    render() {
        return (
            <div>
                <button onClick={this._callBackend}>Delete your city</button>
                <InlineError error={this.state.error} />
            </div>
        )
    }

    _callBackend() {
        this.setState({
            error: '',
        })

        axios
            .delete('/api/city')
            .then(result => {
                // сделать что-нибудь с результатом в том случае, если запрос оказался успешным
            })
            .catch(err => {
                if (err.response.data.error === 'GENERIC') {
                    this.props.setError(err.response.data.description)
                } else {
                    this.setState({
                        error: err.response.data.description,
                    })
                }
            })
    }
}

export default SpecificErrorRequest  
  

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

▍Ошибки, возникающие в клиентской части приложения

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

ОБОЙТИ ОШИБКИ В КОДЕ

В поле ничего нет, мы сообщаем об этом пользователю

Вот код файла SpecificErrorFrontend.js
, реализующий вышеописанный функционал.

   import React, { Component } from 'react'
import axios from 'axios'

import InlineError from './InlineError'

class SpecificErrorRequest extends Component {
    constructor(props) {
        super(props)

        this.state = {
            error: '',
            city: '',
        }

        this._callBackend = this._callBackend.bind(this)
        this._changeCity = this._changeCity.bind(this)
    }

    render() {
        return (
            <div>
                <input
                    type="text"
                    value={this.state.city}
                    style={{ marginRight: 15 }}
                    onChange={this._changeCity}
                />
                <button onClick={this._callBackend}>Delete your city</button>
                <InlineError error={this.state.error} />
            </div>
        )
    }

    _changeCity(e) {
        this.setState({
            error: '',
            city: e.target.value,
        })
    }

    _validate() {
        if (!this.state.city.length) throw new Error('Please provide a city name.')
    }

    _callBackend() {
        this.setState({
            error: '',
        })

        try {
            this._validate()
        } catch (err) {
            return this.setState({ error: err.message })
        }

        axios
            .delete('/api/city')
            .then(result => {
                // сделать что-нибудь с результатом в том случае, если запрос оказался успешным
            })
            .catch(err => {
                if (err.response.data.error === 'GENERIC') {
                    this.props.setError(err.response.data.description)
                } else {
                    this.setState({
                        error: err.response.data.description,
                    })
                }
            })
    }
}

export default SpecificErrorRequest  
  

▍Интернационализация сообщений об ошибках с использованием кодов ошибок

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

Читайте также:  UNARC DII КОД ОШИБКИ 1 ПРИ УСТАНОВЛЕНИИ ИГРЫ КАК ИСПРАВИТЬ

Проброс исключения

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

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

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

Ошибку, о которой catch
не знает, он не должен обрабатывать.

Такая техника называется «проброс исключения»
: в catch(e)
мы анализируем объект ошибки, и если он нам не подходит, то делаем throw e
.

При этом ошибка «выпадает» из try..catch
наружу. Далее она может быть поймана либо внешним блоком try..catch
(если есть), либо «повалит» скрипт.

В примере ниже catch
обрабатывает только ошибки SyntaxError
, а остальные – выбрасывает дальше:

   var data = '{ "name": "Вася", "age": 30 }'; // данные корректны

try {

  var user = JSON.parse(data);

  if (!user.name) {
    throw new SyntaxError("Ошибка в данных");
  }

  blabla(); // произошла непредусмотренная ошибка

  alert( user.name );

} catch (e) {

  if (e.name == "SyntaxError") {
    alert( "Извините, в данных ошибка" );
  } else {
    throw e;
  }

}  
  

Заметим, что ошибка, которая возникла внутри блока catch
, «выпадает» наружу, как если бы была в обычном коде.

В следующем примере такие ошибки обрабатываются ещё одним, «более внешним» try..catch
:

   function readData() {
  var data = '{ "name": "Вася", "age": 30 }';

  try {
    // ...
    blabla(); // ошибка!
  } catch (e) {
    // ...
    if (e.name != 'SyntaxError') {
      throw e; // пробрасываем
    }
  }
}

try {
  readData();
} catch (e) {
  alert( "Поймал во внешнем catch: " + e ); // ловим
}  
  

В примере выше try..catch
внутри readData
умеет обрабатывать только SyntaxError
, а внешний – все ошибки.

Без внешнего проброшенная ошибка «вывалилась» бы в консоль с остановкой скрипта.

Дебаг и поиск ошибок

Время на прочтение

ОБОЙТИ ОШИБКИ В КОДЕ

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

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

Глобальный catch

Зависит от окружения

Информация из данной секции не является частью языка JavaScript.

Давайте представим, что произошла фатальная ошибка (программная или что-то ещё ужасное) снаружи try..catch
, и скрипт упал.

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

Такого способа нет в спецификации, но обычно окружения предоставляют его, потому что это весьма полезно. Например, в Node.js для этого есть process.on("uncaughtException")

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

   window.onerror = function(message, url, line, col, error) {
  // ...
};  
  
message
Сообщение об ошибке.
url
URL скрипта, в котором произошла ошибка.
line
, col
Номера строки и столбца, в которых произошла ошибка.
error
Объект ошибки.
   <script>
  window.onerror = function(message, url, line, col, error) {
    alert(`${message}\n В ${line}:${col} на ${url}`);
  };

  function readData() {
    badFunc(); // Ой, что-то пошло не так!
  }

  readData();
</script>  
  

Роль глобального обработчика window.onerror
обычно заключается не в восстановлении выполнения скрипта – это скорее всего невозможно в случае программной ошибки, а в отправке сообщения об ошибке разработчикам.

Существуют также веб-сервисы, которые предоставляют логирование ошибок для таких случаев, такие как https://errorception.com
или http://www.muscula.com
.

Они работают так:

  1. Мы регистрируемся в сервисе и получаем небольшой JS-скрипт (или URL скрипта) от них для вставки на страницы.
  2. Этот JS-скрипт ставит свою функцию window.onerror
    .
  3. Когда возникает ошибка, она выполняется и отправляет сетевой запрос с информацией о ней в сервис.
  4. Мы можем войти в веб-интерфейс сервиса и увидеть ошибки.

Синтаксис «try…catch»

Конструкция try..catch
состоит из двух основных блоков: try
, и затем catch
:

   try {

  // код...

} catch (err) {

  // обработка ошибки

}  
  

Работает она так:

  1. Сначала выполняется код внутри блока try {...}
    .
  2. Если в нём нет ошибок, то блок catch(err)
    игнорируется: выполнение доходит до конца try
    и потом далее, полностью пропуская catch
    .
  3. Если же в нём возникает ошибка, то выполнение try
    прерывается, и поток управления переходит в начало catch(err)
    . Переменная err
    (можно использовать любое имя) содержит объект ошибки с подробной информацией о произошедшем.

Давайте рассмотрим примеры.

  • Пример без ошибок: выведет alert


    и


    :

       try {
    
      alert('Начало блока try');  // 

    <-- // ...код без ошибок alert('Конец блока try'); //

    <-- } catch(err) { alert('Catch игнорируется, так как нет ошибок'); //

    }

  • Пример с ошибками: выведет


    и


    :

       try {
    
      alert('Начало блока try');  // 

    <-- lalala; // ошибка, переменная не определена! alert('Конец блока try (никогда не выполнится)'); //

    } catch(err) { alert(`Возникла ошибка!`); //

    <-- }

try..catch
работает только для ошибок, возникающих во время выполнения кода

Чтобы try..catch
работал, код должен быть выполнимым. Другими словами, это должен быть корректный JavaScript-код.

Он не сработает, если код синтаксически неверен, например, содержит несовпадающее количество фигурных скобок:

   try {
  {{{{{{{{{{{{
} catch(e) {
  alert("Движок не может понять этот код, он некорректен");
}  
  

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

Таким образом, try..catch
может обрабатывать только ошибки, которые возникают в корректном коде. Такие ошибки называют «ошибками во время выполнения», а иногда «исключениями».

try..catch
работает синхронно

Исключение, которое произойдёт в коде, запланированном «на будущее», например в setTimeout
, try..catch
не поймает:

   try {
  setTimeout(function() {
    noSuchVariable; // скрипт упадёт тут
  }, 1000);
} catch (e) {
  alert( "не сработает" );
}  
  

Это потому, что функция выполняется позже, когда движок уже покинул конструкцию try..catch
.

Чтобы поймать исключение внутри запланированной функции, try..catch
должен находиться внутри самой этой функции:

   setTimeout(function() {
  try {
    noSuchVariable; // try..catch обрабатывает ошибку!
  } catch {
    alert( "ошибка поймана!" );
  }
}, 1000);  
  

Где обитают ошибки

Ошибки в своём коде

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

Ошибки в чужом коде

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

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

Ошибки в библиотеках

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

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

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

Ошибки не воспроизводимые локально

Ошибка воспроизводится на develop стенде или в production, но не воспроизводится локально. Такие ошибки сложнее отлавливать потому что не всегда есть возможность  запустить дебаг на удалённой машине. Поэтому убеждаемся, что ваше окружение соответствует внешнему. 

Проверьте версию приложения

На стенде и локально версии приложения должны совпадать. Возможно на стенде приложение развёрнуто из другой ветки.

Проверьте данные

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

Проверьте соответствие окружений

Если проект на стенде развёрнут в контейнере, то в некоторых IDE (JB RIder) можно дебажить в контейнере

. Если проект развёрнут не в контейнере, то воспроизводимость ошибки может зависеть от окружения. Хотя . Net Core мультиплатформенный фреймворк, не всё что работает под Windows так же работает под Linux. В этом случае либо найти рабочую машину с таким же окружением, либо воспроизвести окружение через контейнеры или виртуальную машину.

Коварные ошибки

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

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

Объект ошибки

В примере выше мы видим объект ошибки. У него есть три основных свойства:

name
Тип ошибки. Например, при обращении к несуществующей переменной: "ReferenceError"
.
message
Текстовое сообщение о деталях ошибки.
stack
Везде, кроме IE8-, есть также свойство stack
, которое содержит строку с информацией о последовательности вызовов, которая привела к ошибке.

В зависимости от браузера у него могут быть и дополнительные свойства, см. Error в MDN
и Error в MSDN
.

Последняя надежда

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

Можно ли как-то узнать о том, что произошло? Да, конечно.

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

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

   <script>
  window.onerror = function(message, url, lineNumber) {
    alert("Поймана ошибка, выпавшая в глобальную область!\n" +
      "Сообщение: " + message + "\n(" + url + ":" + lineNumber + ")");
  };

  function readData() {
    error(); // ой, что-то не так
  }

  readData();
</script>  
  

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

Существуют даже специальные веб-сервисы, которые предоставляют скрипты для отлова и аналитики таких ошибок, например: https://errorception.com/
или https://www.muscula.com/
.

Конструкция try…catch

Конструкция try..catch
состоит из двух основных блоков: try
, и затем catch
:

   try {

  // код ...

} catch (err) {

  // обработка ошибки

}  
  

Работает она так:

  1. Выполняется код внутри блока try
    .

  2. Если в нём ошибок нет, то блок catch(err)
    игнорируется, то есть выполнение доходит до конца try
    и потом прыгает через catch
    .

  3. Если в нём возникнет ошибка, то выполнение try
    на ней прерывается, и управление прыгает в начало блока catch(err)
    .

    При этом переменная err
    (можно выбрать и другое название) будет содержать объект ошибки с подробной информацией о произошедшем.

Таким образом, при ошибке в try
скрипт не «падает», и мы получаем возможность обработать ошибку внутри catch
.

Посмотрим это на примерах.

  • Пример без ошибок: при запуске сработают alert


    и


    :

       try {
    
      alert('Начало блока try');  // 

    <-- // .. код без ошибок alert('Конец блока try'); //

    <-- } catch(e) { alert('Блок catch не получит управление, так как нет ошибок'); //

    } alert("Потом код продолжит выполнение...");

  • Пример с ошибкой: при запуске сработают


    и


    :

       try {
    
      alert('Начало блока try');  // 

    <-- lalala; // ошибка, переменная не определена! alert('Конец блока try'); //

    } catch(e) { alert('Ошибка ' + e.name + ":" + e.message + "\n" + e.stack); //

    <-- } alert("Потом код продолжит выполнение...");

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

Здесь же мы рассматриваем ошибки семантические
, то есть происходящие в корректном коде, в процессе выполнения.

Ошибку, которая произойдёт в коде, запланированном «на будущее», например в setTimeout
, try..catch
не поймает:

   try {
  setTimeout(function() {
    throw new Error(); // вылетит в консоль
  }, 1000);
} catch (e) {
  alert( "не сработает" );
}  
  

На момент запуска функции, назначенной через setTimeout
, этот код уже завершится, интерпретатор выйдет из блока try..catch
.

Чтобы поймать ошибку внутри функции из setTimeout
, и try..catch
должен быть в той же функции.

Проброс исключения

   let json = '{ "age": 30 }'; // данные неполны

try {
  user = JSON.parse(json); // <-- забыл добавить "let" перед user

  // ...
} catch(err) {
  alert("JSON Error: " + err); // JSON Error: ReferenceError: user is not defined
  // (не JSON ошибка на самом деле)
}  
  

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

В нашем случае try..catch
предназначен для выявления ошибок, связанных с некорректными данными. Но по своей природе catch
получает все
свои ошибки из try
. Здесь он получает неожиданную ошибку, но всё также показывает то же самое сообщение "JSON Error"
. Это неправильно и затрудняет отладку кода.

К счастью, мы можем выяснить, какую ошибку мы получили, например, по её свойству name
:

   try {
  user = { /*...*/ };
} catch(e) {
  alert(e.name); // "ReferenceError" из-за неопределённой переменной
}  
  

Есть простое правило:

Блок catch
должен обрабатывать только те ошибки, которые ему известны, и «пробрасывать» все остальные.

Техника «проброс исключения» выглядит так:

  1. Блок catch
    получает все ошибки.
  2. В блоке catch(err) {...}
    мы анализируем объект ошибки err
    .
  3. Если мы не знаем как её обработать, тогда делаем throw err
    .

В коде ниже мы используем проброс исключения, catch
обрабатывает только SyntaxError
:

   let json = '{ "age": 30 }'; // данные неполны
try {

  let user = JSON.parse(json);

  if (!user.name) {
    throw new SyntaxError("Данные неполны: нет имени");
  }

  blabla(); // неожиданная ошибка

  alert( user.name );

} catch(e) {

  if (e.name == "SyntaxError") {
    alert( "JSON Error: " + e.message );
  } else {
    throw e; // проброс (*)
  }

}  
  

Ошибка в строке (*)
из блока catch
«выпадает наружу» и может быть поймана другой внешней конструкцией try..catch
(если есть), или «убьёт» скрипт.

Таким образом, блок catch
фактически обрабатывает только те ошибки, с которыми он знает, как справляться, и пропускает остальные.

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

   function readData() {
  let json = '{ "age": 30 }';

  try {
    // ...
    blabla(); // ошибка!
  } catch (e) {
    // ...
    if (e.name != 'SyntaxError') {
      throw e; // проброс исключения (не знаю как это обработать)
    }
  }
}

try {
  readData();
} catch (e) {
  alert( "Внешний catch поймал: " + e ); // поймал!
}  
  

Здесь readData
знает только, как обработать SyntaxError
, тогда как внешний блок try..catch
знает, как обработать всё.

Дополнительные материалы

Алгоритм отладки

  1. Проверь гипотезу – если гипотеза проверку не прошла то п.3.

  2. Убедись что исправлено – если не исправлено, то п.3.

Подробнее ознакомиться с ним можно в докладе Сергея Щегриковича «Отладка как процесс».

Чем искать ошибки, лучше не допускать ошибки. Прочитайте статью « Качество вместо контроля качества

», чтобы узнать как это делать.

Использование «try…catch»

Давайте рассмотрим реальные случаи использования try..catch
.

Как мы уже знаем, JavaScript поддерживает метод JSON.parse(str)
для чтения JSON.

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

Мы получаем их и вызываем JSON.parse
вот так:

   let json = '{"name":"John", "age": 30}'; // данные с сервера

let user = JSON.parse(json); // преобразовали текстовое представление в JS-объект

// теперь user - объект со свойствами из строки
alert( user.name ); // John
alert( user.age );  // 30  
  

Вы можете найти более детальную информацию о JSON в главе Формат JSON, метод toJSON
.

Если json
некорректен, JSON.parse
генерирует ошибку, то есть скрипт «падает».

Устроит ли нас такое поведение? Конечно нет!

Получается, что если вдруг что-то не так с данными, то посетитель никогда (если, конечно, не откроет консоль) об этом не узнает. А люди очень не любят, когда что-то «просто падает» без всякого сообщения об ошибке.

Давайте используем try..catch
для обработки ошибки:

   let json = "{ некорректный JSON }";

try {

  let user = JSON.parse(json); // <-- тут возникает ошибка...
  alert( user.name ); // не сработает

} catch (e) {
  // ...выполнение прыгает сюда
  alert( "Извините, в данных ошибка, мы попробуем получить их ещё раз." );
  alert( e.name );
  alert( e.message );
}  
  

Добавить комментарий

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