Скидки до 55% и 3 курса в подарок 0 дней 09 :23 :01 Выбрать курс
Код Справочник по фронтенду
#статьи

Исключения в JavaScript: обработка ошибок с помощью try, catch, throw, finally

Ловим и обрабатываем ошибки с использованием JavaScript-инструментов.

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

Сегодня вы узнаете, что такое исключения в JavaScript, какие виды ошибок и исключений бывают и как они обрабатываются с помощью механизма try…catch, а также напишете своё первое исключение на JavaScript.

Содержание


Что такое исключения и ошибки в JavaScript

Исключения (exceptions) в JavaScript — это ошибки в процессе компиляции или выполнении кода. Например, пользователь не заполнил на сайте обязательное поле «Имя» и нажал кнопку «Зарегистрироваться» — случилось исключение.

Создатели JS предусмотрели такие случаи. Чтобы программа не вылетала после ошибки, в коде используют специальные языковые конструкции — try, catch, finally. Ещё это называют обработкой исключений. Если не обработать исключение, скрипт может сломаться и дальнейшая работа сервиса станет непредсказуемой.

try {
  // Код, где может произойти ошибка
} catch (error) {
  // Выполнится, если ошибка произошла
} finally {
  // Дополнительный блок
  // Выполнится после всех событий
}

Какие бывают ошибки

В JavaScript бывают разные ошибки, и за каждый вид отвечает определённый класс:

  • SyntaxError — синтаксическая ошибка (например, забыта скобка).
// Пропущены закрывающие скобки 
eval("function test() { console.log('Hello'");
  • ReferenceError — ошибка ссылки (обращение к несуществующей переменной — адрес ссылки, которого нет).
// Обращение к несуществующей переменной — адрес ссылки, которого нет)
console.log(nonExistentVariable);
  • TypeError — ошибка типа (неверный тип данных).
// Попытка вызвать число как функцию
let num = 16;
num();
  • RangeError — ошибка диапазона (обращение к несуществующему индексу массива).
// Отрицательная длина массива
let arr = new Array(-5);
  • URIError — возникает при работе функций encodeURI, decodeURI, если они получают неверный аргумент.
// Символ % без кода — неправильный URI
decodeURIComponent('%');
  • EvalError — возникает при некорректной работе eval(), функции, которая берёт строку кода и выполняет её.
// Искусственно создаём EvalError
throw new EvalError("Некорректное использование eval()");
  • AggregateError — возникает, когда несколько ошибок происходит одновременно.
// Ждёт первый успешный промис
Promise.any([
  Promise.reject("Ошибка A"),
  Promise.reject("Ошибка B")
])
.catch(e => console.log(e));

Конструкция try-catch в исключениях JavaScript

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

try {
    // Код, который может вызвать ошибку
} catch (error) {
    // Код, который выполняется при появлении ошибки в блоке try
    console.error("Ошибка: ", error.message);
}

Если запустить этот код без try…catch, вылетит исключение.

start(); // Такой функции нет
console.log("Привет"); // Не выполнится

Весь код JavaScript содержит только эти две строки. Никакого метода start() в JavaScript не существует, и в консоли после выполнения этой строки вы увидите Uncaught ReferenceError: start is not defined, а строчка с сообщением «Привет» и весь дальнейший код не будут выполнены.

Скриншот: Google Chrome / Skillbox Media

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

try {
    start(); // Uncaught ReferenceError: start is not defined
    console.log("Привет"); // Этот код не выполнится
} catch (error) {
    console.error("Ошибка поймана. Исключение:", error.message);
}
console.log("Ещё раз привет"); // Этот код выполнится

Исключение обработано. Теперь код выполнится дальше, но следующие строки кода в try не выполнятся, как и console.log("Привет");. Далее мы увидим действия из catch, а потом код продолжит выполняться.

Вывод в консоль будет таким:

❌index.html:23 Ошибка поймана. Исключение: start is not defined
Ещё раз привет

В JavaScript блок catch может быть только один, в отличие от Java, C++ или Python. Это связано с особенностью языка — он упрощён и адаптирован для разработки веб-сервисов.

Если нужно обработать несколько разных исключений за раз, используйте конструкцию switch или if…else для проверки типа внутри catch:

try {
    // Код, который может вызвать исключение
} catch (error) {
    switch (error.constructor) {
        case TypeError:
            // TypeError-исключение
            console.error("Ошибка типа:", error.message);
            break;
        case ReferenceError:
            // ReferenceError-исключение
            console.error("Ошибка ссылки:", error.message);
            break;
        case SyntaxError:
            // SyntaxError-исключение
            console.error("Синтаксическая ошибка:", error.message);
            break;
        default:
            console.error("Неизвестная ошибка:", error.message);
    }
}

Блок finally в исключениях JavaScript

Блок finally — дополнительный блок в конструкции try…catch. Он выполняется в любом случае, было исключение или его не было, и пишется после блока catch. Такой блок необходим для завершения жизненно важных операций в случаях, когда произошла ошибка, всё сломалось и программа дальше работать не будет, но нужно сохранить данные или восстановить состояние.

В следующем JavaScript-коде всё хорошо, ошибок нет: catch не срабатывает, но событие в finally выполняется:

try {
    // Тут всё хорошо
    console.log("Начинаем выполнение");
    let result = 2 + 2;
    console.log("Результат:", result);
} catch (error) {
    // Никаких ошибок не было — код в catch не выполнится
    console.error("Ошибка:", error.message);
} finally {
    // Выполнится в любом случае
    console.log("Этот блок finally выполняется в любом случае");
}

Консоль JavaScript:

Начинаем выполнение
Результат: 4
Этот блок finally выполняется в любом случае

Есть ещё одна особенность: можно обойтись без catch и использовать только try и finally.

try {
    JSON.parse("невалидный JSON"); // Ошибка!
} finally {
    console.log("Финальный блок"); // Выполнится всегда
}

console.log("После try-finally"); // Не выполнится

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

Консоль JavaScript:

Финальный блок

Объект Error и его свойства в JavaScript

Функция console.error("string", error) нужна, чтобы выводить в консоль форматированные ошибки. Она принимает два параметра, один из которых сама ошибка — объект класса Error.

Инфографика: Skillbox Media

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

У класса Error есть несколько основных свойств:

  • name — имя класса ошибки;
  • message — сообщение ошибки;
  • stack — показать стек вызовов функций.
const err = new Error("Неверный тип"); // В конструкторе пишется message
console.log(err.name); // Имя класса ошибки
console.log(err.message); // Сообщение ошибки
console.log(err.stack); // Трассировка стека
  • cause — ссылка на другую ошибку (другой Error), причина другой ошибки.
const original = new Error("Исходная ошибка");
const errWithCause = new Error("Новая ошибка", { cause: original });
console.log(errWithCause.cause); //Error: Исходная ошибка
console.log(errWithCause.message); // Error: Новая ошибка

Выброс исключения throw в JavaScript

По капотом исключения работают так:

  • Где-то на стороне пользователя происходит ошибка.
  • Вылетает исключение в блоке try JavaScript, текущий код прерывается.
  • Программа ищет нужный класс ошибок Error.
  • Передаёт найденную ошибку в catch.
  • Выполняется finally (если есть).
  • Программа продолжает работу после блока try…catch.

Если мы натыкаемся на какой-либо Error, где-то в JavaScript выполняется строчка:

// С помощью throw создаётся и выбрасывается ошибка
throw new TypeError("Сообщение об ошибке");

Можно создавать собственные ошибки. Исключение в JavaScript создаётся с помощью ключевого слова throw:

// Создаём свою ошибку и ловим её с помощью try ...catch
try{
    throw new Error("Новая ошибка");
}catch (error) {
    console.log(error.message); // Error: Новая ошибка
}

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

Как правильно обрабатывать асинхронные ошибки в JavaScript

Асинхронный код в JavaScript — это код, который выполняется не сразу, а позже, после завершения других операций. Хотя JavaScript работает в одном потоке и выполняет команды по порядку, он умеет откладывать задачи, не мешая основному процессу. Это возможно благодаря очереди задач и механизму Event Loop.

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

// Через 3 секунды произойдёт ошибка
// try...catch здесь работать не будет!
try{
  setTimeout(() => {
    throw new Error("Асинхронная ошибка");
  }, 3000);
}catch(error) { console.error(error.message); }

console.log("Скоро будет конец..."); // Эта надпись появится за 3 секунды до исключения

В этом коде try…catch не сработает, потому что ошибка произойдёт позже, уже вне текущего блока. Сообщение «Скоро будет конец…» появится сразу, а ошибка — через 3 секунды, и она не будет обработана.

Чтобы правильно обрабатывать такие ошибки, нужно использовать другие подходы. Один из них — промисы (Promise). Это объект, который описывает результат асинхронной операции: если всё прошло успешно — вызывается resolve, если произошла ошибка — reject. Для обработки ошибок у промиса есть метод catch().

Вот как можно переписать пример с использованием промиса:

new Promise((_, reject) => {
  setTimeout(() => {
    reject(new Error("Асинхронная ошибка"));
  }, 3000);
})
.catch(error => {
  console.error("Ошибка поймана:", error.message);
});

console.log("Скоро будет конец...");

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

Для удобства можно использовать async/await — это синтаксис, который делает асинхронный код более читаемым, как будто он работает синхронно. Но даже в этом случае ошибки нужно оборачивать в try…catch внутри async-функции.

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

// Объявляем асинхронную функцию для получения товаров
async function getProducts() {
  try {
    // Создаём промис, который «симулирует» работу сервера
    // setTimeout через 3 секунды вызовет reject — то есть имитацию ошибки
    await new Promise((resolve, reject) => {
      setTimeout(() => {
        // Здесь специально выбрасываем ошибку
        reject(new Error("Сервер не отвечает"));
      }, 3000);
    });

    // Если бы промис завершился успешно (resolve), код пошёл бы дальше
    // Например:
    // console.log("Данные получены!");

  } catch (error) {
    // Блок catch перехватывает ошибку и выводит сообщение о ней
    console.log("Ошибка при получении данных:", error.message);
  }
}

// Вызываем функцию
getProducts();

// Эта строка выполнится сразу, так как асинхронная функция не блокирует поток
console.log("Запрос отправлен, ждём ответа от сервера...");
  • getProducts() — в нашем случае асинхронная функция, но async делает её похожей на синхронную.
  • await — говорит, что нужно подождать, пока ответит сервер.
  • reject(new Error()) — выбрасывает исключение, если «сервер не отвечает».
  • try…catch — в данном примере поймает исключение, так как мы используем ключевые слова async/await. await можно и нужно оборачивать в try…catch, чтобы выловить исключение.

Практика: проектируем систему ошибок в приложении JavaScript

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

Шаг 1. Создаём HTML-файл для практики

Создайте файл index.html с базовой структурой:

<!DOCTYPE html>
<html lang="ru">
  <head>
    <meta charset="UTF-8" />
    <title>Исключения JavaScript</title>
    <script>
      // Здесь будет ваш JavaScript-код
    </script>
  </head>
  <body>
    <h1>Практика: Проектирование системы ошибок в приложении JavaScript</h1>
  </body>
</html>

В реальных проектах JavaScript обычно выносится в отдельный JS-файл, но для учебных целей мы пишем код прямо внутри тега <script>.

Шаг 2. Создаём базовый класс ошибок приложения

Добавим в блок <script> собственный класс AppError, который будет основой для всех ошибок в нашем приложении:

class AppError extends Error {
  constructor(message, { code = "APP_ERROR", meta = {} } = {}) {
    super(message);
    this.name = this.constructor.name;
    this.code = code;
    this.meta = meta;
  }
}

Этот класс позволяет задавать имя ошибки, код и дополнительные метаданные — удобно для логирования и отладки.

Шаг 3. Создаём специализированную ошибку — NetworkError

Теперь создадим класс NetworkError, который будет использоваться при отсутствии интернет-соединения:

class NetworkError extends AppError {
  constructor(message = "Нет интернета", meta = {}) {
    super(message, { code: "NETWORK_ERROR", meta });
  }
}

Такая ошибка пригодится, если ваше приложение зависит от сетевых запросов.

Шаг 4. Пишем асинхронную функцию загрузки данных

Добавим функцию loadData(), которая имитирует сетевой запрос. Она использует ключевое слово async, а при отсутствии интернета выбрасывает исключение NetworkError.

async function loadData() {
  const online = false; // Имитируем отсутствие интернета

  if (!online) throw new NetworkError(); // Выбрасываем ошибку, если соединения нет

  return ["object01", "object02"]; // Возвращаем данные, если соединение есть
}

Здесь видно, как проверка сетевого состояния встраивается в логику приложения.

Шаг 5. Вызываем функцию и обрабатываем исключения

Теперь вызовем loadData() внутри анонимной асинхронной функции. Мы используем конструкцию try…catch, чтобы перехватить возможные ошибки.

(async () => {
  try {
    const data = await loadData(); // Ожидаем результата
    console.log(data); // Выводим данные в консоль
  } catch (error) {
    if (error instanceof NetworkError) {
      console.warn("Проверьте подключение к интернету."); // Сообщение для пользователя
    } else {
      console.error("Ошибка:", error); // Лог других ошибок
    }
  }
})();

Шаг 6. Проверяем результат в консоли браузера

Откройте файл index.html в браузере и включите DevTools (обычно клавиша F12). Перейдите на вкладку Console.

Вы увидите предупреждение:

Проверьте подключение к интернету.

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

Частые ошибки при работе с исключениями в JavaScript


Использовали await, но не защитили асинхронный код

// Объявляем асинхронную функцию fetchData
async function fetchData() { 
    // Делаем HTTP-запрос к API по указанному адресу и ждём ответа
    const response = await fetch('https://example.com/api/data'); 
    
    // Преобразуем ответ из формата JSON в объект JavaScript
    const data = await response.json(); 
    
    // Выводим полученные данные в консоль
    console.log(data); 
} 

// Вызываем функцию, чтобы запустить загрузку данных
fetchData();

Если ссылка будет некорректной или сервер вернёт ошибку, то программа зависнет или прекратит свою работу. Всегда обёртывайте асинхронный код. В текущем примере с async и await нам будет достаточно использовать try…catch:

// Объявляем асинхронную функцию для получения данных
async function fetchData() {
  try {
    // Делаем HTTP-запрос по адресу 'https://example.com/api/data'
    // fetch возвращает промис, поэтому используем await
    const response = await fetch('https://example.com/api/data');

    // Ответ от сервера нужно преобразовать в обычный объект/массив
    // .json() тоже возвращает промис, поэтому снова ставим await
    const data = await response.json();

    // Выводим полученные данные в консоль
    console.log(data);

  } catch (error) {
    // Если произошла ошибка (например, нет интернета или сервер не отвечает),
    // то она попадёт сюда
    console.error("Ошибка:", error.message);
  }
}

// Запускаем функцию
fetchData();

Проигнорировали ошибки в промисах

// Функция fetchJSON принимает URL и возвращает промис
// 1) Вызываем fetch(url) — отправляем запрос на сервер
// 2) Когда ответ придёт, вызываем response.json()
//    Это тоже возвращает промис, который преобразует данные в объект/массив
const fetchJSON = url => fetch(url).then(response => response.json());

// Вызываем функцию и передаём URL
fetchJSON('https://example.com/bad-data') 
    // Когда данные успешно получены и преобразованы в JSON,
    // они попадают в .then(data => ...)
    .then(data => console.log(data));

Функция fetch() — отвечает за отправку HTTP-запросов в JavaScript. Мы использовали функцию then() — она возвращает промис, но мы не указали функцию промиса catch() — в которой как раз будет обработана ошибка, если что-то пойдёт не так. Правильно написать код будет так:

// Вызываем функцию и передаём некорректный адрес
fetchJSON('https://example.com/bad-data') 
    // Если запрос успешный и данные корректные — попадём сюда
    .then(data => console.log(data)) 
    // Если произошла ошибка (например, сервер вернул плохие данные
    // или JSON нельзя разобрать) — сработает .catch()
    .catch(error => console.error("Ошибка:", error.message));

Повторили обработчик ошибок

Следующий код сильно нагружает систему постоянной проверкой try…catch каждого элемента массива и делает JavaScript-код непонятным:

// Создаём массив чисел от 1 до 10
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// Перебираем массив методом forEach
numbers.forEach(num => { 
    try {
        // Проверяем: если число нечётное — выбрасываем ошибку
        if (num % 2 !== 0) throw new Error(`${num} нечётное`);

        // Если число чётное — выводим его квадрат
        console.log(num * num); 
    } catch (error) {
        // Если поймали ошибку (нечётное число) — выводим сообщение
        console.error("Ошибка:", error.message); 
    }
});

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

// Создаём массив чисел от 1 до 10
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// Перебираем массив методом forEach
numbers.forEach(num => { 
    try {
        // Проверяем: если число нечётное — выбрасываем ошибку
        if (num % 2 !== 0) throw new Error(`${num} нечётное`);

        // Если число чётное — выводим его квадрат
        console.log(num * num); 
    } catch (error) {
        // Если поймали ошибку (нечётное число) — выводим сообщение
        console.error("Ошибка:", error.message); 
    }
});

Когда использовать исключения

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

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

Работа с сетью, файловой системой, базой данных

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

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

try {
  // Отправляем HTTP-запрос на сервер
  // await заставляет JS ждать ответа от сервера
  const response = await fetch('https://api.example.com/data');

  // Преобразуем ответ в формат JSON
  // response.json() тоже возвращает промис, поэтому await нужен
  const data = await response.json();

  // Если всё прошло успешно, выводим данные в консоль
  console.log(data);

} catch (error) {
  // Если что-то пошло не так (сеть недоступна, сервер вернул ошибку,
  // JSON не удалось разобрать), управление попадает сюда
  console.error('Не удалось получить данные:', error.message);

  // Можно вызвать функцию для показа ошибки пользователю
  // Например, показать сообщение на странице
  showErrorMessage('Сервис временно недоступен');
}

Парсинг входящих данных

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

Если попробовать разобрать такую строку без проверки, программа может остановиться с ошибкой. Чтобы этого не произошло, нужно использовать try…catch — так можно перехватить исключение и продолжить работу, даже если данные оказались испорченными.

// Некорректный JSON
const userInput = '{ "Username": "Username123" }';

try {
  // Пробуем разобрать JSON-строку в объект JavaScript
  const obj = JSON.parse(userInput);

  // Пытаемся обратиться к свойству name объекта
  // Если в JSON такого поля нет, получим undefined
  console.log(obj.name);

} catch (error) {
  // Если JSON некорректный, выполнение перейдёт сюда
  // Например, если забыты кавычки или синтаксис неверный
  console.error('Ошибка парсинга JSON:', error.message);
}

Когда можно обойтись без исключений


Отсутствие ожидаемых данных

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

// Массив объектов пользователей
const users = [
  { id: 1, name: 'Ann' },
  { id: 2, name: 'Kate' }
];

// Пытаемся найти пользователя с id = 3
// Метод .find() возвращает первый элемент, который подходит под условие
// Если такого элемента нет, вернёт undefined
const user = users.find(u => u.id === 3);

// Проверяем, найден ли пользователь
if (!user) {
  // Если user === undefined (не найден) — выводим сообщение
  console.log('Пользователь не найден');
} else {
  // Если пользователь найден — выводим его имя
  console.log('Имя пользователя:', user.name);
}

Валидация данных от пользователя

Проверка данных, которые вводит пользователь, — это обычная часть работы приложения. Если, например, человек ввёл email без символа @, это не сбой программы, а просто некорректный ввод.

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

// Функция проверяет, содержит ли строка символ @
// Простая проверка для валидности email
function isEmail(value) {
  return value.includes('@'); // true, если есть @, иначе false
}

// Пример введённого пользователем email
const email = 'testmail.com';

// Проверяем email с помощью функции isEmail
if (!isEmail(email)) {
  // Если функция вернула false (нет @) — показываем сообщение об ошибке
  showErrorMessage('Введите корректный email');
} else {
  // Если функция вернула true, считаем email корректным
  console.log('Email принят');
}

Что ещё почитать

1. MDN Web Docs: управление потоком и обработка ошибок
Объясняет, как работают try…catch, throw, finally и в каких случаях их использовать. Есть примеры синхронного и асинхронного кода, а также рекомендации по стилю.

2. MDN Web Docs: объект Error и его типы
Подробно описывает встроенные типы ошибок: Error, TypeError, SyntaxError, ReferenceError и другие. Рассказывает, как создавать свои классы ошибок и какие свойства доступны.

3. MDN Web Docs: Promise.prototype.catch()
Показывает, как правильно обрабатывать ошибки в промисах. Есть примеры использования .catch() и объяснение, почему его нельзя пропускать.

4. ECMAScript Language Specification (ECMA-262)
Официальная спецификация языка JavaScript. В ней описано поведение исключений на уровне стандарта: как работает throw, как интерпретатор обрабатывает ошибки и какие объекты участвуют в процессе.

Больше интересного про код — в нашем телеграм-канале.  Подписывайтесь!





Курс с помощью в трудоустройстве

Профессия Фронтенд-разработчик

Освойте фронтенд без опыта в IT. Практикуйтесь на реальных задачах и находите первых заказчиков в комьюнити Skillbox.

Узнать о курсе →

Курс с трудоустройством: «Профессия Фронтенд-разработчик» Узнать о курсе
Понравилась статья?
Да

Пользуясь нашим сайтом, вы соглашаетесь с тем, что мы используем cookies 🍪

Ссылка скопирована