Исключения ·
‹ Предыдущий Следующий ›
⏱ 5 минут чтения Обновлено: 2026-10-30

Создание собственного класса исключения в Java

Класс исключения скомпилировался, выбрасывается и перехватывается — а e.getMessage() всё равно печатает null. Так бывает, если сохранить текст ошибки в собственном поле и забыть вызвать super(message):

public class ValidationException extends Exception {
    private String message;

    public ValidationException(String message) {
        this.message = message; // super(message) не вызван
    }
}

// где-то в коде:
try {
    throw new ValidationException("Email не может быть пустым");
} catch (ValidationException e) {
    System.out.println(e.getMessage()); // Вывод: null
}

Собственное (пользовательское, custom) исключение в Java — это класс, унаследованный от Exception (проверяемое исключение) или от RuntimeException (непроверяемое). Чтобы создать свое исключение, достаточно объявить класс с суффиксом Exception, выбрать родителя и добавить конструкторы, передающие сообщение и причину в super(...). Всё остальное — getMessage(), getCause(), printStackTrace() — класс получит по наследству.

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

Чтобы создать класс исключения, объявите класс, который наследует Exception или RuntimeException, и добавьте конструктор, вызывающий super(message). Реализовывать какие-либо методы не нужно: минимальное собственное исключение состоит из объявления класса и одного конструктора.

public class InvalidAgeException extends Exception {
    public InvalidAgeException(String message) {
        super(message);
    }
}

Такое исключение выбрасывают оператором throw, а поскольку оно наследует Exception, метод обязан объявить его в throws:

public class AgeCheck {
    static void checkAge(int age) throws InvalidAgeException {
        if (age < 0 || age > 150) {
            throw new InvalidAgeException("Недопустимый возраст: " + age);
        }
        System.out.println("Возраст принят: " + age);
    }

    public static void main(String[] args) {
        try {
            checkAge(25);
            checkAge(-3);
        } catch (InvalidAgeException e) {
            System.out.println("Ошибка: " + e.getMessage());
        }
    }
}

Вывод:

Возраст принят: 25
Ошибка: Недопустимый возраст: -3

Откуда берутся методы

Сам класс Exception объявляет только конструкторы. Методы getMessage(), getCause(), getStackTrace(), printStackTrace() и toString() он наследует от Throwable — поэтому и ваш подкласс получает их без единой строки кода. Полный список методов — в уроке Иерархия исключений в Java.

extends Exception или extends RuntimeException

Наследуйте Exception, если вызывающий код может и должен обработать ситуацию (компилятор заставит его это сделать), и RuntimeException, если исключение сигнализирует об ошибке программиста или нарушении контракта метода. Родитель определяет, будет ли исключение проверяемым (checked) или непроверяемым (unchecked).

Родительский класс Тип исключения Нужен ли throws / try-catch Когда выбирать
extends Exception Проверяемое (checked) Да, иначе ошибка компиляции unreported exception ... must be caught or declared to be thrown Ситуация ожидаема и восстановима: платёж отклонён, файл не найден, неверный пароль
extends RuntimeException Непроверяемое (unchecked) Нет Нарушение контракта или ошибка в логике: некорректный аргумент, недопустимое состояние объекта
extends Error Непроверяемое Нет Не используйте: Error зарезервирован для проблем уровня JVM (OutOfMemoryError, StackOverflowError)
extends Throwable Проверяемое Да Не используйте: такое исключение не поймает привычный catch (Exception e)

Подробно о том, какие классы относятся к проверяемым и непроверяемым, — в уроке Иерархия исключений в Java.

Практическое правило

Спросите себя: «Что разумного может сделать вызывающий код, поймав это исключение?» Если есть осмысленная реакция (повторить запрос, показать пользователю сообщение, выбрать другой путь) — делайте исключение проверяемым. Если единственный вариант — исправить код, выбирайте RuntimeException. Современные фреймворки (Spring, Hibernate) почти все свои исключения делают непроверяемыми.

Конструкторы исключения: message и cause

Конструкторы не наследуются, поэтому каждый нужный вариант надо объявить в своём классе и передать параметры в super(...). Обычно достаточно двух: с сообщением (String message) и с сообщением и причиной (String message, Throwable cause) — второй позволяет не потерять исходное исключение при обёртывании.

Класс Exception (как и RuntimeException) предоставляет пять конструкторов, которые можно вызвать через super:

Конструктор Что сохраняется Когда использовать
Exception() Ничего: getMessage() вернёт null Редко: когда сам тип исключения говорит всё
Exception(String message) Текст ошибки Основной случай
Exception(String message, Throwable cause) Текст и исходное исключение Обёртывание низкоуровневого исключения (SQL, IO, парсинг) в своё
Exception(Throwable cause) Причину; сообщение = cause.toString() Когда к причине нечего добавить
protected Exception(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) Всё перечисленное плюс флаги (Java 7+) Особые случаи: исключение без stack trace или без suppressed-исключений

Пример класса исключения с двумя конструкторами и цепочкой исключений (exception chaining): NumberFormatException оборачивается в собственное исключение, а исходная причина остаётся доступна через getCause().

public class OrderProcessingException extends Exception {
    public OrderProcessingException(String message) {
        super(message);
    }

    public OrderProcessingException(String message, Throwable cause) {
        super(message, cause);
    }
}
public class QuantityParser {
    static int parseQuantity(String input) throws OrderProcessingException {
        try {
            return Integer.parseInt(input);
        } catch (NumberFormatException e) {
            throw new OrderProcessingException("Некорректное количество товара: " + input, e);
        }
    }

    public static void main(String[] args) {
        try {
            parseQuantity("12a");
        } catch (OrderProcessingException e) {
            System.out.println(e.getMessage());
            System.out.println("Причина: " + e.getCause());
        }
    }
}

Вывод:

Некорректное количество товара: 12a
Причина: java.lang.NumberFormatException: For input string: "12a"

Если бы в конструктор не передали e, вторая строка вывела бы Причина: null, а в stack trace пропал бы блок Caused by: java.lang.NumberFormatException — при отладке пришлось бы угадывать, что именно пошло не так.

Исключение с дополнительными полями и toString()

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

public class MyException extends Exception {
    private final int detail;

    public MyException(int detail, String message) {
        super(message);
        this.detail = detail;
    }

    public int getDetail() {
        return detail;
    }

    @Override
    public String toString() {
        return "MyException{"
                + "detail=" + detail
                + ", message=" + getMessage()
                + "}";
    }
}
public class MyExceptionExample {
    public static void main(String[] args) {
        try {
            compute(1);
            compute(20);
        } catch (MyException e) {
            System.out.println("Перехвачено исключение: " + e);
        }
    }

    public static void compute(int a) throws MyException {
        System.out.println("Вызван метод compute(" + a + ")");
        if (a > 10) {
            throw new MyException(a, "Значение больше 10");
        }
        System.out.println("Нормальное завершение.");
    }
}

Вывод:

Вызван метод compute(1)
Нормальное завершение.
Вызван метод compute(20)
Перехвачено исключение: MyException{detail=20, message=Значение больше 10}

При конкатенации "..." + e вызывается toString(). Без переопределения сработала бы реализация из Throwable, которая возвращает «полное имя класса: сообщение», и последняя строка выглядела бы так: Перехвачено исключение: MyException: Значение больше 10 (для класса в пакете — с именем пакета). Поле detail в неё не попало бы, но осталось бы доступно через e.getDetail().

Пример непроверяемого исключения: extends RuntimeException

Непроверяемое собственное исключение создаётся так же, только родителем указывается RuntimeException. Объявлять его в throws и оборачивать вызов в try-catch не обязательно: компилятор этого не проверяет, а перехватывать исключение можно там, где это действительно нужно.

public class InsufficientFundsException extends RuntimeException {
    private final long shortage;

    public InsufficientFundsException(long shortage) {
        super("Недостаточно средств, не хватает: " + shortage);
        this.shortage = shortage;
    }

    public long getShortage() {
        return shortage;
    }
}
public class Account {
    private long balance = 100;

    public void withdraw(long amount) { // throws не нужен
        if (amount > balance) {
            throw new InsufficientFundsException(amount - balance);
        }
        balance -= amount;
    }

    public static void main(String[] args) {
        Account account = new Account();
        account.withdraw(30);           // баланс 70
        try {
            account.withdraw(500);
        } catch (InsufficientFundsException e) {
            System.out.println(e.getMessage());
            System.out.println("Сумма к пополнению: " + e.getShortage());
        }
    }
}

Вывод:

Недостаточно средств, не хватает: 430
Сумма к пополнению: 430

Если убрать try-catch, программа скомпилируется, но завершится с Exception in thread "main" InsufficientFundsException: Недостаточно средств, не хватает: 430 и stack trace. С проверяемым исключением тот же код без try-catch или throws не скомпилировался бы.

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

Свое исключение стоит создавать, когда ошибка имеет смысл в предметной области (OrderNotFoundException, WrongPasswordException), когда нужно передать дополнительные данные или когда вызывающий код должен перехватывать именно эту ошибку отдельно от других. Для типовых нарушений контракта лучше взять стандартное исключение из JDK — его смысл понятен любому Java-разработчику.

Ситуация Стандартное исключение
Недопустимое значение аргумента метода IllegalArgumentException
Аргумент равен null, хотя не должен NullPointerException (через Objects.requireNonNull(arg, "..."))
Метод вызван в неподходящем состоянии объекта (например, соединение уже закрыто) IllegalStateException
Индекс вне допустимого диапазона IndexOutOfBoundsException (через Objects.checkIndex(index, length), Java 9+)
Операция не поддерживается реализацией UnsupportedOperationException

Типичный сценарий для собственного исключения — граница слоёв приложения: слой доступа к данным перехватывает SQLException и выбрасывает своё DataAccessException с причиной, чтобы бизнес-логика не зависела от деталей JDBC. Если исключений много, их объединяют общим родителем (например, ShopException и его подклассы) — тогда можно перехватить всю группу одним catch. Помните, что catch подкласса должен стоять раньше catch родителя, иначе код не скомпилируется: подробнее — в разделе Несколько блоков catch и их порядок.

На чём чаще всего ошибаются

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

  • Своё поле message вместо super(message). Как в примере в начале урока: getMessage() и stack trace покажут null. Исправление — удалить поле и вызвать super(message) в конструкторе.
  • Потеря причины. throw new MyException("Ошибка БД") внутри catch (SQLException e) теряет исходное исключение: его stack trace не попадёт в лог. Пишите throw new MyException("Ошибка БД", e) и объявляйте конструктор с Throwable cause.
  • Наследование от Throwable или Error. Первое не поймает catch (Exception e), второе вводит в заблуждение: Error означает сбой JVM, а не ошибку приложения.
  • Имя без суффикса Exception. Класс WrongPassword формально допустим, но по соглашению имена исключений заканчиваются на Exception — так их сразу видно в коде и логах.
  • Предупреждение о serialVersionUID. Throwable реализует Serializable, поэтому IDE и компилятор с флагом -Xlint:serial предупреждают об отсутствии поля. Это не ошибка; чтобы убрать предупреждение, добавьте private static final long serialVersionUID = 1L;.
  • Разные правила throws при переопределении. Переопределённый метод не может объявлять более широкие проверяемые исключения, чем метод родителя, — это касается и ваших классов исключений. Правила разобраны в уроке Исключения при наследовании.

Часто задаваемые вопросы

Можно ли сделать исключение generic-классом или record?

Нет. Класс-наследник Throwable не может иметь параметров типа: class MyException<T> extends Exception даёт ошибку компиляции a generic class may not extend java.lang.Throwable, потому что из-за стирания типов catch не смог бы различить MyException<String> и MyException<Integer>. Record тоже не подходит: он неявно наследует java.lang.Record и не может расширять другой класс.

Обязательно ли добавлять serialVersionUID в класс исключения?

Не обязательно: код компилируется и работает без него. Но Throwable реализует Serializable, и если исключения передаются между JVM (RMI, распределённые кэши) или сериализуются в файлы, явный serialVersionUID защищает от InvalidClassException после изменения класса. Заодно исчезает предупреждение IDE.

Как создать исключение без stack trace, чтобы оно работало быстрее?

Вызовите защищённый конструктор, появившийся в Java 7: super(message, null, false, false). Последний параметр writableStackTrace = false отключает заполнение стека, и создание исключения становится заметно дешевле. Платить за это приходится отладкой: в логе не будет видно, где исключение возникло, поэтому приём оправдан только для исключений, которые выбрасываются очень часто и всегда обрабатываются.

Можно ли объявить класс исключения внутри другого класса?

Да. Обычно это статический вложенный класс: public static class NotFoundException extends RuntimeException внутри сервиса, к которому оно относится; снаружи к нему обращаются как UserService.NotFoundException. Нестатический внутренний класс тоже допустим, если внешний класс не generic, но его экземпляр привязан к объекту внешнего класса, поэтому для исключений его почти не используют.

Практика по теме

Закрепите урок на задачах — у каждой есть уровень и оценка времени.

Все задачи модуля →

Комментарии

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