Пользовательские ошибки расширение error

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

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

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

Что такое ошибки в Node.js

Ошибка в Node.js — это любой экземпляр объекта Error. Общие примеры включают встроенные классы ошибок: ReferenceError, RangeError, TypeError, URIError, EvalError и SyntaxError. Пользовательские ошибки также можно создать путём расширения базового объекта Error, встроенного класса ошибки или другой настраиваемой ошибки. При создании ошибок таким путём нужно передать строку сообщения, описывающую ошибку. К сообщению можно получить доступ через свойство message объекта. Объект Error также содержит свойства name и stack, которые указывают имя ошибки и точку в коде, в которой объект создаётся.

const userError = new TypeError("Something happened!");
console.log(userError.name); // TypeError
console.log(userError.message); // Something happened!
console.log(userError.stack);
/*TypeError: Something happened!
    at Object.<anonymous> (/home/ayo/dev/demo/main.js:2:19)
    <truncated for brevity>
    at node:internal/main/run_main_module:17:47 */

Функции объекта Error можно передать или вернуть из функции. Если бросить его с помощью throw, объект Error станет исключением. Когда вы передаёте ошибку из функции, она переходит вверх по стеку, пока исключение не будет поймано. В противном случае uncaught exception может обвалить всю работу.

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

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

Исключения

Чаще всего ошибки функций обрабатывают путём генерации. В этом случае ошибка становится исключением, после чего её можно поймать где-нибудь в стеке с помощью блока try / catch. Если у ошибки есть разрешение всплывать в стеке, не будучи перехваченной, она преобразуется в формат uncaughtException, что приводит к преждевременному завершению работы приложения. Например, встроенный метод JSON.parse () выдаёт ошибку, если строковый аргумент не является допустимым объектом JSON.

function parseJSON(data) {
  return JSON.parse(data);
}
try {
  const result = parseJSON('A string');
} catch (err) {
  console.log(err.message); // Unexpected token A in JSON at position 0
}

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

function square(num) {
  if (typeof num !== 'number') {
    throw new TypeError(`Expected number but got: ${typeof num}`);
  }
  return num * num;
}
try {
  square('8');
} catch (err) {
  console.log(err.message); // Expected number but got: string
}

Колбэк с первым аргументом-ошибкой

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

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

function (err, result) {}

Первый аргумент зарезервирован для объекта ошибки. Если ошибка возникает в ходе асинхронной операции, она доступна через аргумент err при неопределённом результате. Однако, если ошибки не возникает, err будет иметь значение null или undefined, а result будет содержать ожидаемый результат операции. Этот шаблон работает, если прочитать содержимое файла с помощью встроенного метода fs.readFile ():

const fs = require('fs');
fs.readFile('/path/to/file.txt', (err, result) => {
  if (err) {
    console.error(err);
    return;
  }
  // Log the file contents if no error
  console.log(result);
});

Метод readFile () использует колбэк в качестве своего последнего аргумента, который, в свою очередь, соответствует подписи функции «первая ошибка». В этом сценарии result включает в себя содержимое файла, который читается, если ошибки не возникает. В противном случае он определяется как undefined, а аргумент err заполняется объектом ошибки, содержащим информацию о проблеме: файл не найден или недостаточно полномочий.

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

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

function square(num, callback) {
  if (typeof callback !== 'function') {
    throw new TypeError(`Callback must be a function. Got: ${typeof callback}`);
  }
  // simulate async operation
  setTimeout(() => {
    if (typeof num !== 'number') {
      // if an error occurs, it is passed as the first argument to the callback
      callback(new TypeError(`Expected number but got: ${typeof num}`));
      return;
    }
    const result = num * num;
    // callback is invoked after the operation completes with the result
    callback(null, result);
  }, 100);
}

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

Не нужно непосредственно обрабатывать ошибку в функции колбэка. Её можно распространить вверх по стеку, передав на другой колбэк. Но сначала убедитесь, что вы не генерируете исключение внутри функции. Асинхронное исключение невозможно отследить, потому что окружающий блок try / catch завершается до выполнения колбэка. Следовательно, исключение будет распространяться на вершину стека, что приведёт к завершению работы приложения. Исключение — когда обработчик зарегистрирован для process.on ('uncaughtException').

try {
  square('8', (err, result) => {
    if (err) {
      throw err; // not recommended
    }
    console.log(result);
  });
} catch (err) {
  // This won't work
  console.error("Caught error: ", err);
}

Отклонение обещаний

Обещания в JavaScript — это актуальный способ выполнения асинхронных операций в Node.js. Они предпочтительнее колбэков из-за лучшего потока, который соответствует современным способам анализа программ, особенно с шаблоном async / await. Любой API-интерфейс Node.js, использующий колбэки с ошибкой для асинхронной обработки ошибок, может быть преобразован в обещания с помощью встроенного метода util.promisify (). Например, заставить метод fs.readFile () использовать обещания можно так:

const fs = require('fs');
const util = require('util');
const readFile = util.promisify(fs.readFile);

Переменная readFile — это версия fs.readFile () с обещаниями, в которой отклонения обещаний используются для сообщения об ошибках. Эти ошибки можно отследить, связав метод catch:

readFile('/path/to/file.txt')
  .then((result) => console.log(result))
  .catch((err) => console.error(err));

Также можно использовать обещанные API в функциях async. Так выглядит основной способ использования обещаний в современном JavaScript: в нём код читается как синхронный, и для обработки ошибок применяют знакомый механизм try / catch. Перед асинхронным запуском важно использовать await, чтобы обещание было выполнено или отклонено до того, как функция возобновит выполнение. При отклонении обещания выражение await выбрасывает отклонённое значение, которое впоследствии попадает в окружающий блок catch.

(async function callReadFile() {
  try {
    const result = await readFile('/path/to/file.txt');
    console.log(result);
  } catch (err) {
    console.error(err);
  }
})();

Обещанияможно использовать в асинхронных функциях, возвращая обещание из функции и помещая код функции в обратный вызов обещания. Если есть ошибка, её стоит отклонить (reject) с помощью объекта Error. В противном случае можно разрешить (resolve) обещание с результатом, чтобы оно было доступно в цепочке метода .then или напрямую как значение функции async при использовании async / await.

function square(num) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (typeof num !== 'number') {
        reject(new TypeError(`Expected number but got: ${typeof num}`));
      }
      const result = num * num;
      resolve(result);
    }, 100);
  });
}
square('8')
  .then((result) => console.log(result))
  .catch((err) => console.error(err));

Источники событий

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

const { EventEmitter } = require('events');
function emitCount() {
  const emitter = new EventEmitter();
  let count = 0;
  // Async operation
  const interval = setInterval(() => {
    count++;
    if (count % 4 == 0) {
      emitter.emit(
        'error',
        new Error(`Something went wrong on count: ${count}`)
      );
      return;
    }
    emitter.emit('success', count);
    if (count === 10) {
      clearInterval(interval);
      emitter.emit('end');
    }
  }, 1000);
  return emitter;
}

Функция emitCount () возвращает новый эмиттер событий, который сообщает об успешном исходе в асинхронной операции. Она увеличивает значение переменной count и каждую секунду генерирует событие успеха и событие ошибки, если значение count делится на 4. Когда count достигает 10, генерируется событие завершения. Этот шаблон позволяет передавать результаты по мере их поступления вместо ожидания завершения всей операции.

Вот как можно отслеживать и реагировать на каждое из событий, генерируемых функцией emitCount ():

const counter = emitCount();
counter.on('success', (count) => {
  console.log(`Count is: ${count}`);
});
counter.on('error', (err) => {
  console.error(err.message);
});
counter.on('end', () => {
  console.info('Counter has ended');
});

Функция колбэка для каждого прослушивателя событий выполняется независимо, как только событие генерируется. Событие ошибки (error) — это особый случай для Node.js, потому что при отсутствии прослушивателя процесс Node.js выходит из строя. Вы можете закомментировать прослушиватель событий ошибки выше и запустить программу, чтобы увидеть, что произойдёт.

Расширение объекта ошибки

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

Пользовательские классы ошибок, расширяющие объект Error, сохранят основные свойства ошибки: сообщение (message), имя (name) и стек (stack). Но у них есть собственные свойства. ValidationError можно улучшить, добавив значимые свойства — часть ввода, вызвавшую ошибку.

Вот как можно расширить встроенный объект Error в Node.js:

class ApplicationError extends Error {
  constructor(message) {
    super(message);
    // name is set to the name of the class
    this.name = this.constructor.name;
  }
}
class ValidationError extends ApplicationError {
  constructor(message, cause) {
    super(message);
    this.cause = cause
  }
}

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

function validateInput(input) {
  if (!input) {
    throw new ValidationError('Only truthy inputs allowed', input);
  }
  return input;
}
try {
  validateInput(userJson);
} catch (err) {
  if (err instanceof ValidationError) {
    console.error(`Validation error: ${err.message}, caused by: ${err.cause}`);
    return;
  }
  console.error(`Other error: ${err.message}`);
}

Ключевое слово instanceof следует использовать для проверки конкретного типа ошибки. Не используйте имя ошибки для проверки типа, как в err.name === 'ValidationError': это не сработает, если ошибка получена из подкласса ValidationError.

Типы ошибок

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

Операционные ошибки

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

  • Запрос API не выполняется по какой-либо причине (например, сервер не работает или превышен лимит скорости).

  • Соединение с базой данных потеряно, например, из-за неисправного сетевого соединения.

  • ОС не может выполнить запрос на открытие файла или запись в него.

  • Пользователь отправляет на сервер недопустимые данные: неверный номер телефона или адрес электронной почты.

Ошибки программиста

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

  • Синтаксические ошибки: незакрытая фигурная скобка.

  • Ошибки типа при попытке сделать что-то неправильное: выполнение операций с операндами несовпадающих типов.

  • Неверные параметры при вызове функции.

  • Ссылки на ошибки при неправильном написании имени переменной, функции или свойства.

  • Попытка получить доступ к местоположению за концом массива.

  • Неспособность обработать операционную ошибку.

Обработка операционных ошибок

Операционные ошибки в большинстве случаев предсказуемы. Их обработка — это рассмотрение вероятности неудачного завершения операции, возможных причин и последствий. Рассмотрим несколько стратегий обработки операционных ошибок в Node.js.

Сообщить об ошибке в стек

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

Повторить операцию

Сетевые запросы к внешним службам иногда могут завершаться ошибкой, даже если запрос полностью верен. Это случается из-за сбоя и неполадках сети или перегрузке сервера. Можно повторить запрос несколько раз, пока он не будет успешно завершён или пока не будет достигнуто максимальное количество повторных попыток. Первое, что нужно сделать, — это определить, уместно ли повторить запрос. Если исходный код состояния HTTP ответа — 500, 503 или 429, повторте запрос через некоторое время.

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

Отправить ошибку клиенту

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

Прервать программу.

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

Предотвращение ошибок программиста

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

Принять TypeScript

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

Когда проект на TypeScript, такие ошибки, как undefined is not a function, синтаксические или ссылочные ошибки, исчезают из кодовой базы. Перенос на TypeScript можно выполнять постепенно. Для быстрой миграции есть инструмент ts-migrate.

Определить поведение для неверных параметров

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

При работе с неверными параметрами и определяйте их поведение, либо выдавая ошибку, либо возвращая специальное значение, такое как null, undefined или -1, когда проблема может быть решена локально. Первый вариант— это подход, используемый JSON.parse (), который выдаёт исключение SyntaxError, если строка для синтаксического анализа недействительна. Второй вариант — метод string.indexOf ()

Автоматизированное тестирование

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

Неперехваченные исключения и необработанные отклонения обещаний

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

// unsafe
process.on('uncaughtException', (err) => {
  console.error(err);
});

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

// better
process.on('uncaughtException', (err) => {
  Honeybadger.notify(error); // log the error in a permanent storage
  // attempt a gracefully shutdown
  server.close(() => {
    process.exit(1); // then exit
  });
  // If a graceful shutdown is not achieved after 1 second,
  // shut down the process completely
  setTimeout(() => {
    process.abort(); // exit immediately and generate a core dump file
  }, 1000).unref()
});

Событие unhandledRejection генерируется, когда отклонённое обещание не обрабатывается блоком catch. В отличие от uncaughtException, эти события не вызывают немедленного сбоя приложения. Однако необработанные отклонения обещаний сейчас признаны устаревшими и могут немедленно завершить процесс в следующих релизах Node.js. Отслеживать необработанные отклонения обещаний можно с помощью прослушивателя событий unhandledRejection:

process.on('unhandledRejection', (reason, promise) => {
  Honeybadger.notify({
    message: 'Unhandled promise rejection',
    params: {
      promise,
      reason,
    },
  });
  server.close(() => {
    process.exit(1);
  });
  setTimeout(() => {
    process.abort();
  }, 1000).unref()
});

Серверы необходимо запускать с помощью диспетчера процессов, который автоматически перезапустит их в случае сбоя. Распространённый вариант — PM2, но для Linux существуют также systemd и upstart, а пользователи Docker могут использовать собственную политику перезапуска. По завершении всех процессов стабильное обслуживание будет восстановлено почти мгновенно, а у вас будт информация о неперехваченном исключении. Можно запутсить несколько процессов и применить балансировщик нагрузки для распределения входящих запросов. Это поможет предотвратить простои.

Централизованная отчётность об ошибках

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

Honeybadger предоставляет всё необходимое для отслеживания ошибок. Интегрируется так:

Установите пакет

Используйте npm для установки пакета:

$ npm install @honeybadger-io/js --save

Импортируйте библиотеку

Импортируйте библиотеку и настройте её с помощью ключа API, чтобы получать сообщения об ошибках:

const Honeybadger = require('@honeybadger-io/js');
Honeybadger.configure({
  apiKey: '[ YOUR API KEY HERE ]'
});
Сообщите об ошибках
Метоодом notify ():
try {
  // ...error producing code
} catch(error) {
  Honeybadger.notify(error);
}

Просмотрите полную документацию или ознакомьтесь с образцом Node.js / Express на GitHub.


Без обработки ошибок не бывает надёжного софта. 

Спасибо за внимание и удачного кода!

Create global or application-level Exception handlers and return Custom Error Messages in Spring REST APIs.

Overview

Effective communication is the key to healthy and efficient relationships. Interestingly, the same applies to any Client and Server relationships. The client’s request may succeed or fail on the server. However, the server should provide the most appropriate status code in either of the outcomes.

Although sending a correct status code is enough for a client to take real action based on the outcome of a request, in case of failures, the client may need more details about what went wrong. For example, failure details like the exception type and an error message can help clients log the error or provide appropriate failure messages to their clients.

This article will teach How to handle different failures and return Custom Error Messages from a Spring REST API. If you don’t know how to handle exceptions in Spring REST API, please read Spring Rest Service Exception Handling.

Return a Generic Error Message using @ResponseStatus

The most basic way of returning an error message from a REST API is to use the @ResponseStatus annotation. We can add the error message in the annotation’s reason field. Although we can only return a generic error message, we can specify exception-specific error messages.

Next is an example of a @ControllerAdvice using @ResponseStatus annotations to return exception-specific error messages.

@ControllerAdvice public class ApplicationExceptionHandler { @ResponseStatus( value = HttpStatus.NOT_FOUND, reason = "Requested Student Not Found") @ExceptionHandler(StudentNotFoundException.class) public void handleException(StudentNotFoundException e) { } @ResponseStatus( value = HttpStatus.BAD_REQUEST, reason = "Received Invalid Input Parameters") @ExceptionHandler(InputValidationException.class) public void handleException(InputValidationException e) { } @ResponseStatus( value = HttpStatus.GATEWAY_TIMEOUT, reason = "Upstream Service Not Responding, Try Again") @ExceptionHandler(ServiceUnavailableException.class) public void handleException(ServiceUnavailableException e) { } }

Code language: Java (java)

The Exception handler class has three exception handlers, each of which returns a specific HTTP Response Status. Each response status specifies a reason field with a particular error message.

To view the error message in the response, ensure you have turned on include-messages in the server configuration. To learn more about Spring Boot server configurations, please visit Spring Boot Embedded Tomcat Configuration.

server: error: include-message: always

Code language: YAML (yaml)

Next is an example of a response object the REST API returns. Note that the response object has the specified error message.

{ "timestamp": "", "status": 404, "error": "Not Found", "message": "Requested Student Not Found", "path": "/students/Jack" }

Code language: JSON / JSON with Comments (json)

Although we can specify exception-specific error messages, it is still not informative. Therefore in the following sections, we will learn how to return a more specific error message from Spring REST API.

Return Error Message Using Custom Error Object

Let’s create a class representing the error message and the status code. We will return an instance of that in case of errors.

Next is the Error class representing the status code and a String message. We use a few Lombok annotations that introduce regular getter and setter methods and a constructor using the final fields.

Custom Response Error Class

@Data @RequiredArgsConstructor public class Error { private final HttpStatus httpStatus; private final String message; }

Code language: Java (java)

Now that we have an error model created, we will use it to return a detailed error message from Controller Advice.

@ExceptionHandler(StudentNotFoundException.class) public ResponseEntity handleException( StudentNotFoundException e) { Error error = new Error(HttpStatus.NOT_FOUND, e.getLocalizedMessage()); return new ResponseEntity<>(error, error.getHttpStatus()); }

Code language: Java (java)

The exception handler returns an instance of the Error class populated with the exception message and HTTP Status Code.

Now, we can throw our Not Found Exception with a custom error message.

throw new StudentNotFoundException ("Student service failed, studentId : " + studentId);

Code language: Java (java)

When the REST API cannot find the requested resource, we get a detailed error as a response.

{ "httpStatus": "NOT_FOUND", "message": "Student service failed, studentId : Jack" }

Code language: JSON / JSON with Comments (json)

Return Error Message Using HashMap

Also, instead of creating a dedicated error class, we can return a detailed error message using a simple HashMap. Next is an example of producing and returning a Custom Error Message using Java HashMap.

@ExceptionHandler(StudentNotFoundException.class) public ResponseEntity<Map<String, String>> handleException(StudentNotFoundException e) { Map<String, String> errorResponse = Map.of( "message", e.getLocalizedMessage(), "status", HttpStatus.NOT_FOUND.toString() ); return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); }

Code language: Java (java)

Handle Bad Request Exceptions

The Bad Request errors are the Client errors where the client’s request doesn’t meet the requirements of the target server. This section will see how to handle Bad Request exceptions and provide a custom or detailed error response.

Type Mismatch Exceptions

The Type Mismatch Exceptions occur when Spring Controller cannot map the request parameters, path variables, or header values into controller method arguments. This section covers the handling of MethodArgumentTypeMismatchException and TypeMismatchException.

Spring throws MethodArgumentTypeMismatchException when the controller argument doesn’t have a required type. On the other hand, Spring throws TypeMismatchException when there is a type mismatch while setting Bean properties. Also, both exceptions provide a detailed error message that we can use to prepare the Error object.

To demonstrate that, next is an example of Handling MethodArgumentTypeMismatchException and TypeMismatchException and returning a detailed error message in Controller Advice.

@ExceptionHandler({ MethodArgumentTypeMismatchException.class, TypeMismatchException.class }) public ResponseEntity<Map<String, String>> handleException(TypeMismatchException e) { Map<String, String> errorResponse = Map.of( "message", e.getLocalizedMessage(), "status", HttpStatus.BAD_REQUEST.toString() ); return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); }

Code language: Java (java)

Note that the controller advice catches both exceptions; however, the method arguments accept an exception of type TypeMismatchException because it is the parent of the other exception.

Next, the snippet shows a detailed error message when we call a rest endpoint with an incompatible path variable leading to MethodArgumentTypeMismatchException.

{ "httpStatus": "BAD_REQUEST", "message": "Failed to convert value of type 'java.lang.String' to required type 'java.lang.Long'; nested exception is java.lang.NumberFormatException: For input String: "Jack"" }

Code language: JSON / JSON with Comments (json)

Bean Validation Exceptions

The Bean Validation exceptions occur when the request contents do not pass the provided validations.

The BindException occurs when the binding errors are fatal. While the MethodArgumentNotValidException occurs when validations specified by @Valid fail. Note that the MethodArgumentNotValidException is a subclass of BindException. Thus, we can handle them using the same Spring REST API’s exception handler.

@ExceptionHandler({ BindException.class, MethodArgumentNotValidException.class }) public ResponseEntity<Map<String, Object>> handleException(BindException e) { List<String> errors = new ArrayList<>(); e.getFieldErrors() .forEach(err -> errors.add(err.getField() + ": " + err.getDefaultMessage())); e.getGlobalErrors() .forEach(err -> errors.add(err.getObjectName() + ": " + err.getDefaultMessage())); Map<String, Object> errorResponse = Map.of( "error", errors, "message", e.getLocalizedMessage(), "status", HttpStatus.BAD_REQUEST.toString() ); return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); }

Code language: Java (java)

Here we have created a List<String> to represent individual binding errors and add that to the response Map. Instead, we can add a List<String> field to the Error class we created in the previous section and populate the list with individual errors.

Spring throws HttpMediaTypeNotSupportedException, when a POST, PUT, or PATCH endpoint on the server cannot handle the content type sent by the client. The REST Controllers on the server specify the content type they can support. When the media type that a client sends doesn’t match, the client gets this exception back.

To demonstrate, next is an example of handling HttpMediaTypeNotSupportedException and returning a custom error response.

@ExceptionHandler(HttpMediaTypeNotSupportedException.class) public ResponseEntity<Map<String, String>> handleException( HttpMediaTypeNotSupportedException e) { String provided = e.getContentType().toString(); List<String> supported = e.getSupportedMediaTypes().stream() .map(MimeType::toString) .collect(Collectors.toList()); String error = provided + " is not one of the supported media types (" + String.join(", ", supported) + ")"; Map<String, String> errorResponse = Map.of( "error", error, "message", e.getLocalizedMessage(), "status", HttpStatus.UNSUPPORTED_MEDIA_TYPE.toString() ); return new ResponseEntity<>(errorResponse, HttpStatus.UNSUPPORTED_MEDIA_TYPE); }

Code language: Java (java)

As seen in the exception handler above, the instance of HttpMediaTypeNotSupportedException provides detailed information about the incorrect media type we provided and a list of actually supported media types. Thus, we create a custom error message based on the available information.

{ "error":"text/plain;charset=UTF-8 is not one of the supported media types ( application/octet-stream, text/plain, application/xml, text/xml, application/x-www-form-urlencoded, application/*+xml, multipart/form-data, multipart/mixed, application/json, application/*+json, */*)", "message":"Content type 'text/plain;charset=UTF-8' not supported", "status":"415 UNSUPPORTED_MEDIA_TYPE" }

Code language: JSON / JSON with Comments (json)

The above snippet shows a client’s sample error response when it sends a request with an invalid media type.

Handle Request Body Not Readable Exception

Now we will see an example of handling HttpMessageNotReadableException and returning a custom error response. The HttpMessageNotReadableException occurs when the request body is missing or unreadable.

@ExceptionHandler(HttpMessageNotReadableException.class) public ResponseEntity<Map<String, String>> handleException( HttpMessageNotReadableException e) throws IOException { Map<String, String> errorResponse = Map.of( "message", e.getLocalizedMessage(), "status", HttpStatus.BAD_REQUEST.toString() ); return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); }

Code language: Java (java)

Handle HTTP Request Method Not Supported Exception

The HttpMethodNotSupportedException occurs when the HTTP endpoint on the REST API does not support the HTTP request method. Let’s write an exception handler for HttpMethodNotSupportedException and return a detailed error message.

@ExceptionHandler(HttpRequestMethodNotSupportedException.class) public ResponseEntity<Map<String, String>> handleException( HttpRequestMethodNotSupportedException e) throws IOException { String provided = e.getMethod(); List<String> supported = List.of(e.getSupportedMethods()); String error = provided + " is not one of the supported Http Methods (" + String.join(", ", supported) + ")"; Map<String, String> errorResponse = Map.of( "error", error, "message", e.getLocalizedMessage(), "status", HttpStatus.METHOD_NOT_ALLOWED.toString() ); return new ResponseEntity<>(errorResponse, HttpStatus.METHOD_NOT_ALLOWED); }

Code language: Java (java)

As seen in the exception handler above, the exception instance provides detailed information about the provided HTTP Method and an array of Supported HTTP Methods. We use it to form a clear error message.

{ "error": "GET is not one of the supported Http Methods (POST)", "message": "Request method 'GET' not supported", "status": "405 METHOD_NOT_ALLOWED" }

Code language: JSON / JSON with Comments (json)

The snippet showed an example response when the client attempted to execute a GET endpoint, while the REST API supports only POST.

Default Exception Handler

Similarly, we can create a default exception handler advice that handles all Exception types. Spring attempts to find the most specific handler when we have multiple exception handlers and falls back to the default handler if there is no suitable handler.

@ExceptionHandler(Exception.class) public ResponseEntity<Map<String, String>> handleException( Exception e) throws IOException { Map<String, String> errorResponse = Map.of( "message", e.getLocalizedMessage(), "status", HttpStatus.INTERNAL_SERVER_ERROR.toString() ); return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); }

Code language: Java (java)

Above is an example of writing a default exception handler that returns an error message by the exception instance and an HTTP Status of 500.

Summary

This detailed tutorial taught us how to Return Custom Error Messages in Spring REST API. Firstly, we understood that Spring returns a generic error message and the most suitable HTTP Status Code by default. However, we can write our exception handlers for specific exceptions using @ControllerAdvice and produce a custom and detailed error response.

For more on Spring and Spring Boot Tutorials, please visit Spring Tutorials.


Обработка пользовательских ошибок, расширение Error

Здравствуйте!  Иногда при разработке  часто необходимы собственные классы ошибок для различных вещей, которые могут пойти не так в наших программах. Для ошибок при работе с сетью может понадобиться класс  HttpError, для операций с базой данных класс DbError,а  для поиска –  класс NotFoundError и т.д.

Такие ошибки в JavaScript  должны поддерживать базовые свойства, такие как message, name и  stack. Но также они могут иметь и свои собственные свойства. Например, объекты HttpError могут иметь такое  свойство statusCode со значениями соотвественно  404, 403 или 500.

Язык JavaScript позволяет вызывать метод throw с любыми аргументами, то есть технически классы ошибок не нуждаются в наследовании от  класса Error. Но если использовать наследование, то появляется возможность идентификации объектов ошибок посредством obj instanceof Error. Так что лучше конечно же  применять наследование.

По мере развития  приложения, наши  ошибки образуют иерархию, например, класс HttpTimeoutError может наследовать от класса HttpError ну и так далее.

Пользовательские ошибки в Javascript

Расширение Error

В качестве примера давайте рассмотрим функцию readUser(json), которая должна читать данные пользователя в формате JSON.

Пример того, как может выглядеть корректный json:

let json = `{ "name": "John", "age": 30 }`;

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

Наша функция readUser(json) будет не только читать JSON-данные, но и валидировать их. Если необходимые поля отсутствуют или данные в неверном формате, то это будет ошибкой. Но не синтаксической ошибкой SyntaxError, потому что данные синтаксически корректны. Это будет ошибка другого рода.

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

Наш класс ValidationError должен наследоваться от встроенного класса Error.

Класс Error встроенный, вот его примерный код, просто чтобы мы знали, что расширяем:

// "Псевдокод" встроенного класса Error, определённого самим JavaScript 
class Error { constructor(message) { 
this.message = message; 
this.name = "Error"; // (разные имена для разных встроенных классов ошибок) 
this.stack = <стек вызовов>; // нестандартное свойство, но обычно поддерживается } }

Теперь давайте унаследуем от него ValidationError и попробуем новый класс в действии:

class ValidationError extends Error { 
constructor(message) { 
super(message); // (1) 
this.name = "ValidationError"; // (2) } } 
function test() { throw new ValidationError("Упс!"); } 
try { test(); } 
 catch(err) 
{ alert(err.message); 
// Упс! alert(err.name); 
// ValidationError alert(err.stack); 
// список вложенных вызовов с номерами строк для каждого }

Обратите внимание: в строке 1 мы вызываем родительский конструктор. JavaScript требует от нас вызова метода super в дочернем конструкторе, так что это обязательно. Родительский конструктор устанавливает свойство message.

Родительский конструктор также устанавливает свойство name для «Error», поэтому в строке 2 мы сбрасываем его на правильное значение.

Попробуем использовать его в функции readUser(json):

class ValidationError extends Error { 
constructor(message) { super(message); 
this.name = "ValidationError"; } } 
// Использование function readUser(json) { 
let user = JSON.parse(json); 
if (!user.age) { throw new ValidationError("Нет поля: age"); } 
if (!user.name) { throw new ValidationError("Нет поля: name"); } 
return user; } 
// Рабочий пример с try..catch 
try { let user = readUser('{ "age": 25 }'); } 
catch (err) { if (err instanceof ValidationError) { 
alert("Некорректные данные: " + err.message); // Некорректные данные: Нет поля: name } 
else if (err instanceof SyntaxError) { 
// (*) alert("JSON Ошибка Синтаксиса: " + err.message); } 
else { throw err; // неизвестная ошибка, пробросить исключение (**) } }

Блок try..catch в коде выше обрабатывает и нашу ValidationError, и встроенную SyntaxError из функции JSON.parse.

Обратите внимание, как мы используем метод instanceof для проверки конкретного типа ошибки в строке (*).

Мы можем также проверить тип, используя err.name:

// ... // вместо (err instanceof SyntaxError) } 
else if (err.name == "SyntaxError") { // (*) // ...

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

Также важно, что если catch встречает неизвестную ошибку, то он пробрасывает её в строке (**). Блок catch знает, только как обрабатывать ошибки валидации и синтаксические ошибки, а другие виды ошибок (из-за опечаток в коде и другие непонятные) он должен выпустить соответственно наружу.

Дальнейшее наследование

Класс ValidationError является слишком уж общим. Много чего может пойти не так. Свойство может отсутствовать или иметь неверный формат (например, строка как значение возраста age). Поэтому для отсутствующих свойств сделаем более конкретный класс PropertyRequiredError. Он будет нести дополнительную информацию о свойстве, которое отсутствует.

class ValidationError extends Error {  
constructor(message) { 
super(message); 
this.name = "ValidationError"; 
} } 
class PropertyRequiredError extends ValidationError { 
constructor(property) { 
super("Нет свойства: " + property); 
this.name = "PropertyRequiredError"; 
this.property = property; } } 
// Применение 
function readUser(json) { 
let user = JSON.parse(json); 
if (!user.age) { 
throw new PropertyRequiredError("age"); } 
if (!user.name) { throw new PropertyRequiredError("name"); }
 return user; } 
// Рабочий пример с try..catch 
try { let user = readUser('{ "age": 25 }'); } 
catch (err) { if (err instanceof ValidationError) { 
alert("Неверные данные: " + err.message); 
// Неверные данные: Нет свойства: name 
alert(err.name); // PropertyRequiredError 
alert(err.property); // name } 
else if (err instanceof SyntaxError) { 
alert("Ошибка синтаксиса JSON: " + err.message); } 
else { throw err; // неизвестная ошибка, повторно выбросит исключение } }

Новый класс PropertyRequiredError очень просто использовать: необходимо указать лишь имя свойства new PropertyRequiredError(property). Сообщение для пользователя message генерируется конструктором.

Обратите внимание, что свойство this.name в конструкторе класса PropertyRequiredError снова присвоено вручную. Правда, немного утомительно – присваивать свойство this.name = <class name> в каждом классе пользовательской ошибки. Можно этого избежать, если сделать наш собственный класс ошибки, который будет ставить this.name = this.constructor.name. И затем наследовать все ошибки уже от него.

Давайте назовём его как MyError.

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

class MyError extends Error { constructor(message) { 
super(message); this.name = this.constructor.name; } } 
class ValidationError extends MyError { } 
class PropertyRequiredError extends ValidationError { 
constructor(property) { 
super("Нет свойства: " + property); 
this.property = property; } } 
// name корректное 
alert( new PropertyRequiredError("field").name ); // PropertyRequiredError

Теперь пользовательские ошибки стали намного короче, особенно ValidationError, так как мы избавились от строки «this.name = …» в конструкторе класса.

Обёртывание исключений

Назначение функции readUser в приведённом выше коде – это собственно «чтение данных пользователя». В процессе могут возникнутьи различные виды ошибок. Сейчас у нас есть SyntaxError и ValidationError, но в будущем функция readUser может расшириться и, возможно, генерировать другие типы ошибок.

Код, который вызывает readUser, должен обрабатывать все эти ошибки.

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

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

Итак, давайте создадим новый класс ReadError для представления таких ошибок. Если ошибка возникает внутри readUser, мы её перехватим и сгенерируем ReadError. Мы также сохраним ссылку на исходную ошибку в свойстве cause. Тогда внешний код должен будет только проверить наличие ReadError.

Этот код определяет ошибку ReadError и демонстрирует её использование в readUserи try..catch:

class ReadError extends Error { 
constructor(message, cause) { 
super(message); this.cause = cause; 
this.name = 'ReadError'; } } 
class ValidationError extends Error { /*...*/ } 
class PropertyRequiredError extends ValidationError { /* ... */ } 
function validateUser(user) { if (!user.age) { 
throw new PropertyRequiredError("age"); } 
if (!user.name) { throw new PropertyRequiredError("name"); } } 
function readUser(json) { 
let user; 
try { user = JSON.parse(json); } 
catch (err) { 
if (err instanceof SyntaxError) { 
throw new ReadError("Синтаксическая ошибка", err); } 
else { throw err; } } try { validateUser(user); } 
catch (err) { 
if (err instanceof ValidationError) { 
throw new ReadError("Ошибка валидации", err); } 
else { throw err; } } } 
try { readUser('{bad json}'); } 
catch (e) { if (e instanceof ReadError) { 
alert(e); 
// Исходная ошибка: SyntaxError:Unexpected token b in JSON at position 1 
alert("Исходная ошибка: " + e.cause); } 
else { throw e; } }

В приведённом выше коде функция readUser работает так, как описано – распознаёт синтаксические ошибки и ошибки валидации и выдаёт вместо них ошибки ReadError (неизвестные ошибки, как обычно, пробрасываются).

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

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

Итого

  • Мы можем наследовать свои классы ошибок от Error и других встроенных классов ошибок, но нужно позаботиться о свойстве name и не забыть вызвать метод super.
  • Мы можем использовать instanceof для проверки типа ошибок. Это также работает и с наследованием. Но иногда у нас объект ошибки, возникшей в сторонней библиотеке, и нет простого способа получить класс. Тогда для проверки типа ошибки можно использовать свойство name.
  • Обёртывание исключений является распространённой техникой: функция ловит низкоуровневые исключения и создаёт одно «высокоуровневое» исключение вместо низкоуровневых. Иногда низкоуровневые исключения становятся свойствами этого объекта, как err.cause в примерах выше, но это вовсе  не обязательно.

Задачи

Наследование от класса SyntaxError

Создайте класс FormatError, который наследует от встроенного класса SyntaxError.

Класс должен поддерживать свойства message, name и stack.

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.

Поделиться

Твитнуть

Поделиться

(Visited 283 times, 1 visits today)

Понравилась статья? Поделить с друзьями:
  • Получение сообщила об ошибке 0x800ccc92
  • Пользовательские коды ошибок oracle
  • Получение сообщила об ошибке 0x8004210a
  • Пользовательская ошибка 1 freshtunes
  • Пользователь уже существует код ошибки