Enums, Exceptions, and Robust Error Handling

Designing Custom Exceptions

ReadingPreview

আপনি একটি free preview lesson দেখছেন।

Lesson Overview

Java built-in exceptions অনেক common failure represent করে:

IllegalArgumentException
IllegalStateException
IOException
SQLException

কিন্তু application-এর কিছু failures domain-specific।

Examples:

Course not found
Duplicate enrollment
Course content could not be loaded
Course code already registered
Payment processing failed
Invalid course transition

সব failureকে:

IllegalStateException

দিয়ে represent করলে caller failure-এর exact meaning বুঝতে পারে না।

Custom exception meaningful type provide করে।

Example:

CourseNotFoundException

Caller can now catch:

catch (
        CourseNotFoundException exception
) {
}

এই lesson-এ আমরা শিখব:

  • Custom exception কেন দরকার
  • Meaningful exception naming
  • Checked এবং unchecked custom exceptions
  • Constructors
  • Cause preservation
  • Structured context
  • Domain এবং infrastructure exception distinction
  • Exception hierarchy
  • Overly broad এবং overly granular exception design avoid করা
  • Course, enrollment, এবং repository examples

Learning Objectives

এই lesson শেষে আপনি পারবেন:

  • একটি custom exception class তৈরি করতে
  • Checked বা unchecked base class choose করতে
  • Meaningful constructors add করতে
  • Original cause preserve করতে
  • Domain-specific failure type design করতে
  • Exception hierarchy create করতে
  • Structured context safely expose করতে
  • Excessive custom exceptions avoid করতে
  • Boundary-level exception translation implement করতে

Why Create a Custom Exception?

Consider:

throw new IllegalStateException(
        "Course not found."
);

Caller catches:

catch (
        IllegalStateException exception
) {
}

But IllegalStateException may also mean:

Course has no lessons
Course is already archived
Invalid publication transition
Repository unavailable

The type does not communicate enough।

Custom exception:

throw new CourseNotFoundException(
        courseCode
);

Now the failure meaning is explicit।


Exception Type Is Part of the Contract

A custom exception type communicates:

What category of failure occurred?

Message communicates:

What specific context caused it?

Example:

CourseNotFoundException

is the category।

Course was not found for code JAVA-OOP.

is the contextual message।

Both are useful।


Basic Unchecked Custom Exception

public final class CourseNotFoundException
        extends RuntimeException {

    public CourseNotFoundException(
            String message
    ) {
        super(
                message
        );
    }
}

Usage:

throw new CourseNotFoundException(
        "Course was not found."
);

Because it extends:

RuntimeException

caller is not compiler-forced to catch or declare it।


Basic Checked Custom Exception

public final class CourseImportException
        extends Exception {

    public CourseImportException(
            String message
    ) {
        super(
                message
        );
    }
}

Method:

public Course importCourse(
        Path path
) throws CourseImportException {
}

Caller must catch or declare it।


Choosing Checked or Unchecked

Use an unchecked custom exception when:

  • Failure represents invalid usage
  • Failure represents invalid object state
  • Most callers cannot recover
  • Exception should propagate to an application boundary
  • Checked declarations would create unnecessary noise

Use a checked custom exception when:

  • Every caller should consciously acknowledge the failure
  • Caller has meaningful recovery options
  • The operation is low-level or resource-oriented
  • Catch-or-declare improves correctness

Use a result type when:

  • Failure is expected and common
  • Caller naturally branches on the outcome
  • The operation itself completed correctly

Naming Conventions

Custom exception names should usually end with:

Exception

Good:

CourseNotFoundException
DuplicateEnrollmentException
CourseRepositoryException
CourseImportException
InvalidCourseTransitionException

Weak:

CourseError
Problem
Failure
InvalidThing

The name should describe the failure category clearly।


Avoid Repeating Exception Meaninglessly

Weak:

CourseException

This is too broad unless it is an intentional base class।

Stronger:

CourseNotFoundException
CoursePublicationException
CourseRepositoryException

Use broad names only as hierarchy parents।


Standard Constructors

A useful custom exception often includes:

public CourseRepositoryException(
        String message
) {
    super(
            message
    );
}

and:

public CourseRepositoryException(
        String message,
        Throwable cause
) {
    super(
            message,
            cause
    );
}

The second constructor preserves lower-level failure।


Complete Basic Exception

public final class CourseRepositoryException
        extends RuntimeException {

    public CourseRepositoryException(
            String message
    ) {
        super(
                message
        );
    }

    public CourseRepositoryException(
            String message,
            Throwable cause
    ) {
        super(
                message,
                cause
        );
    }
}

Usage:

catch (
        SQLException exception
) {
    throw new CourseRepositoryException(
            "Could not load course "
            + courseCode
            + ".",
            exception
    );
}

Preserve the Original Cause

Weak:

catch (
        SQLException exception
) {
    throw new CourseRepositoryException(
            "Database operation failed."
    );
}

The original cause is lost।

Better:

catch (
        SQLException exception
) {
    throw new CourseRepositoryException(
            "Database operation failed.",
            exception
    );
}

Now stack trace includes:

Caused by: SQLException

Why Cause Preservation Matters

Without cause:

CourseRepositoryException

may hide whether the original failure was:

Connection timeout
Constraint violation
Authentication failure
Syntax error
Network interruption

Cause chain retains technical diagnosis while higher-level type communicates application meaning।


Custom Exception with Domain Context

public final class CourseNotFoundException
        extends RuntimeException {

    private final CourseCode courseCode;

    public CourseNotFoundException(
            CourseCode courseCode
    ) {
        super(
                "Course was not found for code "
                + courseCode
                + "."
        );

        this.courseCode =
                courseCode;
    }

    public CourseCode getCourseCode() {
        return courseCode;
    }
}

Caller can access structured context:

exception.getCourseCode()

without parsing the message।


Why Structured Context Is Better Than Parsing Text

Weak:

String message =
        exception.getMessage();

// Extract course code from text

Messages may change।

Structured field:

CourseCode courseCode =
        exception.getCourseCode();

is stable and type-safe।


Keep Exception Context Immutable

private final CourseCode courseCode;

Custom context fields should usually be immutable।

Avoid adding setters:

setCourseCode(...)

Exception meaning should not change after creation।


Do Not Store Sensitive Data

Avoid fields or messages containing:

Password
Access token
Payment card number
Private identity data
Full database connection string
Secret API key

Exceptions often reach logs and monitoring systems।

Store only safe diagnostic context।


Domain Exception vs Infrastructure Exception

Domain Exception

Represents a business or domain rule failure।

Examples:

CourseNotFoundException
DuplicateEnrollmentException
InvalidCourseTransitionException

Infrastructure Exception

Represents failure in storage, network, or external systems।

Examples:

CourseRepositoryException
CourseContentStorageException
PaymentGatewayException

These categories may be handled differently at boundaries।


Example Domain Exception

public final class InvalidCourseTransitionException
        extends RuntimeException {

    private final CourseStatus currentStatus;
    private final CourseStatus targetStatus;

    public InvalidCourseTransitionException(
            CourseStatus currentStatus,
            CourseStatus targetStatus
    ) {
        super(
                "Course cannot transition from "
                + currentStatus
                + " to "
                + targetStatus
                + "."
        );

        this.currentStatus =
                currentStatus;

        this.targetStatus =
                targetStatus;
    }

    public CourseStatus getCurrentStatus() {
        return currentStatus;
    }

    public CourseStatus getTargetStatus() {
        return targetStatus;
    }
}

Using the Transition Exception

public void changeStatus(
        CourseStatus targetStatus
) {
    if (targetStatus == null) {
        throw new IllegalArgumentException(
                "Target status is required."
        );
    }

    if (
            !status.canTransitionTo(
                    targetStatus
            )
    ) {
        throw new InvalidCourseTransitionException(
                status,
                targetStatus
        );
    }

    status =
            targetStatus;
}

Here:

Null target → General argument contract violation
Invalid transition → Domain-specific failure

Example Infrastructure Exception

public final class CourseContentStorageException
        extends RuntimeException {

    public CourseContentStorageException(
            String message,
            Throwable cause
    ) {
        super(
                message,
                cause
        );
    }
}

Usage:

try {
    return Files.readString(
            path
    );
} catch (
        IOException exception
) {
    throw new CourseContentStorageException(
            "Could not read content for course "
            + courseCode
            + ".",
            exception
    );
}

Exception Translation Between Layers

Consider layers:

File system
↓
Storage adapter
↓
Application service
↓
API boundary

Low-level failure:

IOException

Storage adapter translates to:

CourseContentStorageException

API boundary translates to:

HTTP 500 response

Each layer communicates in its own abstraction।


Avoid Leaking Technology Details

Weak repository contract:

Course findByCode(
        CourseCode code
) throws SQLException;

Higher layers now know JDBC details।

Stronger repository implementation:

public Course findByCode(
        CourseCode code
) {
    try {
        return executeQuery(
                code
        );
    } catch (
        SQLException exception
    ) {
        throw new CourseRepositoryException(
                "Could not query course "
                + code
                + ".",
                exception
        );
    }
}

Exception Hierarchy

Sometimes related exceptions benefit from a shared parent।

Example:

public abstract class CourseException
        extends RuntimeException {

    protected CourseException(
            String message
    ) {
        super(
                message
        );
    }

    protected CourseException(
            String message,
            Throwable cause
    ) {
        super(
                message,
                cause
        );
    }
}

Child:

public final class CourseNotFoundException
        extends CourseException {
}

Another child:

public final class CourseRepositoryException
        extends CourseException {
}

Why Use a Shared Parent?

Boundary can catch all course-related failures:

catch (
        CourseException exception
) {
}

Or specific ones first:

catch (
        CourseNotFoundException exception
) {
} catch (
        CourseException exception
) {
}

A hierarchy is useful when failures share handling behavior।


Catch Ordering with Custom Hierarchy

Correct:

try {
    service.load(
            code
    );
} catch (
        CourseNotFoundException exception
) {
    // 404
} catch (
        CourseException exception
) {
    // General course failure
}

Specific child must come first।


Do Not Create a Hierarchy Without a Handling Need

Weak design:

ApplicationException
├── DomainException
│   ├── CourseException
│   │   ├── CourseStateException
│   │   │   └── InvalidCourseTransitionException

If no caller catches these parent levels meaningfully, the hierarchy adds complexity without value।

Start simple।

Add a parent when shared handling actually exists।


One Exception per Message Is Too Much

Avoid creating exceptions such as:

CourseTitleBlankException
CourseIdNegativeException
CourseHasZeroLessonsException
CourseAlreadyPublishedException

for every validation sentence।

Built-in exceptions may be sufficient:

IllegalArgumentException
IllegalStateException

Create custom types when callers need to distinguish the category programmatically or when the type expresses an important domain boundary।


Too Few Exception Types Is Also Weak

Weak:

throw new ApplicationException(
        "Something failed."
);

Everything becomes indistinguishable।

Balance:

  • Built-in exceptions for generic contracts
  • Custom domain exceptions for meaningful categories
  • Custom infrastructure exceptions at abstraction boundaries
  • Result types for expected outcomes

Custom Exceptions Should Add Meaning

A custom exception should provide at least one of:

  • Meaningful failure category
  • Structured context
  • Abstraction translation
  • Shared handling type
  • Stable application contract

If it only renames:

RuntimeException

without adding meaning, it may not be worthwhile।


Course Not Found: Result or Exception?

Two methods may have different contracts।

Optional Lookup

public Course findByCode(
        CourseCode code
)

Possible:

Return Course
Return null

Absence is expected।

Required Lookup

public Course requireByCode(
        CourseCode code
)

Possible:

Course course =
        findByCode(
                code
        );

if (course == null) {
    throw new CourseNotFoundException(
            code
    );
}

return course;

Method name communicates that existence is required।


Complete CourseNotFoundException

package io.liveklass.course;

public final class CourseNotFoundException
        extends RuntimeException {

    private final CourseCode courseCode;

    public CourseNotFoundException(
            CourseCode courseCode
    ) {
        super(
                "Course was not found for code "
                + courseCode
                + "."
        );

        this.courseCode =
                courseCode;
    }

    public CourseCode getCourseCode() {
        return courseCode;
    }
}

Catalog with Optional and Required Lookup

public final class CourseCatalog {

    private final Map<CourseCode, Course> coursesByCode;

    public CourseCatalog() {
        this.coursesByCode =
                new HashMap<>();
    }

    public Course findByCode(
            CourseCode code
    ) {
        if (code == null) {
            throw new IllegalArgumentException(
                    "Course code is required."
            );
        }

        return coursesByCode.get(
                code
        );
    }

    public Course requireByCode(
            CourseCode code
    ) {
        Course course =
                findByCode(
                        code
                );

        if (course == null) {
            throw new CourseNotFoundException(
                    code
            );
        }

        return course;
    }
}

Duplicate Enrollment Exception

public final class DuplicateEnrollmentException
        extends RuntimeException {

    private final long learnerId;
    private final CourseCode courseCode;

    public DuplicateEnrollmentException(
            long learnerId,
            CourseCode courseCode
    ) {
        super(
                "Learner "
                + learnerId
                + " is already enrolled in course "
                + courseCode
                + "."
        );

        this.learnerId =
                learnerId;

        this.courseCode =
                courseCode;
    }

    public long getLearnerId() {
        return learnerId;
    }

    public CourseCode getCourseCode() {
        return courseCode;
    }
}

Should Duplicate Enrollment Throw?

It depends on the contract।

Result-Based Contract

EnrollmentResult enroll(...)

Use when duplicate enrollment is expected and frequent।

Exception-Based Contract

void enroll(...)

Use when method promises to create enrollment and duplicate command violates that contract।

Do not choose exception only because it sounds more serious।


Checked Import Exception Example

Course import may require caller action।

public final class CourseImportException
        extends Exception {

    private final Path sourcePath;

    public CourseImportException(
            Path sourcePath,
            String message
    ) {
        super(
                message
        );

        this.sourcePath =
                sourcePath;
    }

    public CourseImportException(
            Path sourcePath,
            String message,
            Throwable cause
    ) {
        super(
                message,
                cause
        );

        this.sourcePath =
                sourcePath;
    }

    public Path getSourcePath() {
        return sourcePath;
    }
}

Method:

public Course importCourse(
        Path path
) throws CourseImportException {
}

Import Service Example

public final class CourseImportService {

    public String importContent(
            Path path
    ) throws CourseImportException {
        if (path == null) {
            throw new IllegalArgumentException(
                    "Import path is required."
            );
        }

        try {
            return Files.readString(
                    path
            );
        } catch (
                IOException exception
        ) {
            throw new CourseImportException(
                    path,
                    "Could not import course content.",
                    exception
            );
        }
    }
}

Caller must explicitly handle or declare CourseImportException


Structured Error Codes

Sometimes exception type alone is not enough।

Possible:

public enum EnrollmentErrorCode {

    ALREADY_ENROLLED,
    COURSE_NOT_AVAILABLE,
    LEARNER_SUSPENDED
}

Exception:

public final class EnrollmentException
        extends RuntimeException {

    private final EnrollmentErrorCode code;

    public EnrollmentException(
            EnrollmentErrorCode code,
            String message
    ) {
        super(
                message
        );

        this.code = code;
    }

    public EnrollmentErrorCode getCode() {
        return code;
    }
}

Boundary can map code to a stable response।


Type vs Error Code

Possible designs:

Separate Exception Types

DuplicateEnrollmentException
CourseUnavailableException
LearnerSuspendedException

Good when each failure has distinct handling and context।

One Exception with Error Code

EnrollmentException
+ EnrollmentErrorCode

Good when failures share a boundary and stable code mapping।

Avoid creating both a deep hierarchy and many codes without a clear need।


Exception Message Is Not an API Code

Do not write:

if (
        exception.getMessage()
                .contains(
                        "already enrolled"
                )
) {
}

Messages are for humans and diagnostics।

Use:

  • Exception type
  • Enum error code
  • Structured field

for programmatic decisions।


API Boundary Mapping Example

try {
    enrollmentService.enroll(
            learnerId,
            courseCode
    );

    return createdResponse();
} catch (
        DuplicateEnrollmentException exception
) {
    return conflictResponse(
            "ALREADY_ENROLLED",
            exception.getMessage()
    );
} catch (
        CourseNotFoundException exception
) {
    return notFoundResponse(
            "COURSE_NOT_FOUND",
            exception.getMessage()
    );
} catch (
        CourseRepositoryException exception
) {
    logError(
            exception
    );

    return serverErrorResponse();
}

Different exception categories become different external responses।


Do Not Return Raw Internal Messages Automatically

Exception message may contain safe diagnostic context, but boundary should still decide what external message to expose।

Internal:

Could not query course JAVA-OOP using PostgreSQL.

External:

Course information is temporarily unavailable.

Exception type helps mapping without exposing implementation details।


Serialization Warning

Custom exceptions are normal objects, but you usually should not serialize the entire exception directly into an API response।

Avoid exposing:

  • Stack trace
  • Cause chain
  • Internal fields
  • Class names
  • Storage paths

Create a dedicated error response model instead।


Logging Custom Exceptions

Log once at a boundary that owns failure reporting।

Example:

catch (
        CourseRepositoryException exception
) {
    logger.error(
            "Course repository operation failed.",
            exception
    );

    return serverErrorResponse();
}

Avoid logging and rethrowing at every layer unless each log adds unique operational value।


Constructor Validation in Exceptions

Suppose:

public CourseNotFoundException(
        CourseCode courseCode
)

Should it accept null?

Usually no।

this.courseCode =
        Objects.requireNonNull(
                courseCode,
                "Course code is required."
        );

But ensure message construction does not happen before validation।

Example:

public CourseNotFoundException(
        CourseCode courseCode
) {
    super(
            createMessage(
                    courseCode
            )
    );

    this.courseCode =
            courseCode;
}

private static String createMessage(
        CourseCode courseCode
) {
    if (courseCode == null) {
        throw new IllegalArgumentException(
                "Course code is required."
        );
    }

    return "Course was not found for code "
            + courseCode
            + ".";
}

In practice, exception constructors should remain simple।


Keep Custom Exceptions Lightweight

Exceptions should not:

  • Query databases
  • Call services
  • Read files
  • Send notifications
  • Perform expensive calculations

They should mainly hold:

  • Type
  • Message
  • Cause
  • Safe context

Avoid Mutable Collections Inside Exceptions

Weak:

private final List<String> validationErrors;

and returning the same mutable list।

Better:

this.validationErrors =
        List.copyOf(
                validationErrors
        );

Getter:

public List<String> getValidationErrors() {
    return validationErrors;
}

Because stored list is already immutable।


Validation Exception with Multiple Errors

public final class CourseValidationException
        extends RuntimeException {

    private final List<String> errors;

    public CourseValidationException(
            List<String> errors
    ) {
        super(
                createMessage(
                        errors
                )
        );

        if (
                errors == null
                || errors.isEmpty()
        ) {
            throw new IllegalArgumentException(
                    "Validation errors are required."
            );
        }

        this.errors =
                List.copyOf(
                        errors
                );
    }

    public List<String> getErrors() {
        return errors;
    }

    private static String createMessage(
            List<String> errors
    ) {
        if (
                errors == null
                || errors.isEmpty()
        ) {
            return "Course validation failed.";
        }

        return "Course validation failed with "
                + errors.size()
                + " error(s).";
    }
}

This can represent aggregate validation failure।


Exception or Validation Result?

For interactive forms, returning all validation errors may be normal behavior।

Possible:

ValidationResult

instead of throwing।

Custom validation exception is more appropriate when:

  • Method contract requires valid input
  • Validation failure aborts an operation
  • Boundary centrally translates the exception
  • Caller does not need local branching on every field

Again, contract matters।


Complete Example: Course Service

CourseNotFoundException.java

package io.liveklass.course;

public final class CourseNotFoundException
        extends RuntimeException {

    private final CourseCode courseCode;

    public CourseNotFoundException(
            CourseCode courseCode
    ) {
        super(
                "Course was not found for code "
                + courseCode
                + "."
        );

        this.courseCode =
                courseCode;
    }

    public CourseCode getCourseCode() {
        return courseCode;
    }
}

CourseRepositoryException.java

package io.liveklass.course;

public final class CourseRepositoryException
        extends RuntimeException {

    public CourseRepositoryException(
            String message,
            Throwable cause
    ) {
        super(
                message,
                cause
        );
    }
}

CourseRepository.java

package io.liveklass.course;

public interface CourseRepository {

    Course findByCode(
            CourseCode courseCode
    );
}

CourseService.java

package io.liveklass.course;

public final class CourseService {

    private final CourseRepository repository;

    public CourseService(
            CourseRepository repository
    ) {
        if (repository == null) {
            throw new IllegalArgumentException(
                    "Course repository is required."
            );
        }

        this.repository =
                repository;
    }

    public Course requireCourse(
            CourseCode courseCode
    ) {
        if (courseCode == null) {
            throw new IllegalArgumentException(
                    "Course code is required."
            );
        }

        Course course =
                repository.findByCode(
                        courseCode
                );

        if (course == null) {
            throw new CourseNotFoundException(
                    courseCode
            );
        }

        return course;
    }
}

Boundary Handling Example

public String handleCourseRequest(
        String rawCourseCode
) {
    try {
        CourseCode courseCode =
                new CourseCode(
                        rawCourseCode
                );

        Course course =
                courseService.requireCourse(
                        courseCode
                );

        return course.getTitle();
    } catch (
        IllegalArgumentException exception
    ) {
        return "Invalid course code.";
    } catch (
        CourseNotFoundException exception
    ) {
        return exception.getMessage();
    } catch (
        CourseRepositoryException exception
    ) {
        return "Course information is temporarily unavailable.";
    }
}

Design Review

Why Use IllegalArgumentException for Invalid Code?

It is a general caller contract violation।

No custom exception is necessary unless callers need a distinct invalid-code category।

Why Use CourseNotFoundException?

Required lookup failed and boundary needs to map it distinctly।

Why Use CourseRepositoryException?

Technical persistence failure should not look like normal absence।

Why Keep Cause in Repository Exception?

Operational debugging still needs the original storage failure।


Common Mistakes

Creating a Custom Exception for Every Validation Message

Produces excessive classes without meaningful handling differences।


Using One Generic Application Exception

Makes failure categories indistinguishable।


Forgetting the Cause Constructor

Prevents proper exception wrapping।


Parsing Exception Messages

Messages are not stable programmatic contracts।


Storing Sensitive Context

May leak secrets through logs।


Adding Mutable Context

Exception meaning can change after creation।


Building Deep Hierarchies Without Shared Handling

Adds complexity without benefit।


Throwing Checked Exceptions That No Caller Can Handle

Creates declaration pollution।


Making Expected Business Outcomes Exception-Driven

Makes normal control flow noisy।


Logging at Every Layer

Creates duplicate logs।


Exposing Internal Exception Objects Directly to API Clients

Leaks implementation details।


Doing Work Inside Exception Constructors

Exceptions should remain lightweight।


Practice Exercises

Exercise 1: Course Not Found

Create:

CourseNotFoundException

Requirements:

  • Extend RuntimeException
  • Store CourseCode
  • Generate a clear message
  • Provide getter

Exercise 2: Repository Exception

Create:

CourseRepositoryException

Requirements:

  • Extend RuntimeException
  • Support message-only constructor
  • Support message-and-cause constructor

Exercise 3: Checked Import Exception

Create:

CourseImportException

Requirements:

  • Extend Exception
  • Store source Path
  • Preserve optional cause
  • Provide getter

Exercise 4: Invalid Transition

Create:

InvalidCourseTransitionException

Store:

Current status
Target status

Use it in a course status change method।


Exercise 5: Choose Built-In or Custom

Choose the best exception:

  1. Blank course title
  2. Course not found during required lookup
  3. Course publishing from invalid state
  4. Database query failure
  5. Negative learner ID
  6. Duplicate enrollment requiring distinct API handling

Explain each choice।


Exercise 6: Error Code Design

Create:

EnrollmentException

with:

EnrollmentErrorCode

Values:

ALREADY_ENROLLED
COURSE_NOT_AVAILABLE
LEARNER_SUSPENDED

Exercise 7: Preserve the Cause

Catch:

IOException

and throw:

CourseContentStorageException

Include course code and preserve the cause।


Exercise 8: Simplify a Hierarchy

Given:

ApplicationException
DomainException
LearningException
CourseException
CourseStateException
CoursePublishStateException

Decide which levels can be removed and explain why।


Predict the Result

Question 1

public final class TestException
        extends RuntimeException {
}

Must callers catch or declare it?

Answer

No।

It is unchecked।


Question 2

public final class TestException
        extends Exception {
}

Must callers catch or declare it?

Answer

Yes।

It is checked।


Question 3

throw new CourseRepositoryException(
        "Load failed.",
        ioException
);

Where is ioException stored?

Answer

As the exception cause।

It can be accessed with:

getCause()

Question 4

Can two custom exception types have the same message?

Answer

Yes।

Programmatic distinction comes from the type, not only the message।


Question 5

Should caller logic search exception message text to decide an HTTP status?

Answer

No।

Use exception type, structured field, or stable error code।


Knowledge Check

Question 1

Why create a custom exception?

Question 2

When should it extend RuntimeException?

Question 3

When should it extend Exception?

Question 4

Why include a cause constructor?

Question 5

What is exception translation?

Question 6

What is structured exception context?

Question 7

Why should context fields be immutable?

Question 8

Why should sensitive data not be stored in exceptions?

Question 9

When is a shared exception parent useful?

Question 10

Why avoid one exception class per validation message?

Question 11

Why avoid one generic exception for all failures?

Question 12

Why should messages not be parsed programmatically?

Question 13

When is a result type better than a custom exception?

Question 14

Why should custom exception constructors stay lightweight?

Question 15

What should an API boundary expose instead of a raw exception?


Knowledge Check Answers

Answer 1

To communicate a meaningful failure category, add context, translate abstractions, or enable distinct handling।

Answer 2

When callers are not compiler-forced to handle it, most intermediate layers cannot recover, or the failure represents a contract or application-level failure।

Answer 3

When every caller should deliberately acknowledge and choose a recovery path।

Answer 4

To preserve the original technical failure and stack trace chain।

Answer 5

Catching a lower-level exception and throwing one meaningful to the current abstraction।

Answer 6

Typed fields such as course code, learner ID, or current status stored on the exception।

Answer 7

The failure meaning should remain stable after creation।

Answer 8

Exceptions often reach logs, monitoring, and support tools।

Answer 9

When multiple related failures share meaningful handling behavior।

Answer 10

It creates excessive classes without distinct contracts or handling needs।

Answer 11

Callers cannot distinguish important failure categories।

Answer 12

Messages are human-readable and may change; they are not stable contracts।

Answer 13

When the outcome is expected, common, and part of normal business flow।

Answer 14

They should represent failure, not perform services, I/O, or expensive work।

Answer 15

A safe dedicated error response with stable code and user-appropriate message।


Lesson Summary

এই lesson-এ আমরা শিখেছি:

  • Custom exception meaningful application failure types তৈরি করে
  • Exception type এবং message different purposes serve করে
  • Unchecked custom exceptions RuntimeException extend করে
  • Checked custom exceptions Exception extend করে
  • Checked or unchecked choice caller action এবং API contract-এর ওপর depend করে
  • Exception names usually Exception suffix ব্যবহার করে
  • Cause constructor lower-level failure preserve করে
  • Structured context message parsing-এর চেয়ে safer
  • Exception context immutable হওয়া উচিত
  • Sensitive information exception messages বা fields-এ রাখা উচিত নয়
  • Domain exceptions business rule failures represent করে
  • Infrastructure exceptions storage, network, or external-system failures represent করে
  • Exception translation abstraction boundaries protect করে
  • Shared parent exception common handling support করতে পারে
  • Deep hierarchy shared handling ছাড়া unnecessary
  • Every validation message-এর জন্য custom class প্রয়োজন নেই
  • One generic exception সব failuresকে indistinguishable করে
  • Optional lookup এবং required lookup different contracts রাখতে পারে
  • Expected business outcomes result types দিয়ে model করা যেতে পারে
  • Stable error codes programmatic handling improve করে
  • Exception messagesকে API codes হিসেবে ব্যবহার করা উচিত নয়
  • API boundaries raw exception serialize না করে safe error response তৈরি করবে
  • Custom exceptions lightweight এবং focused হওয়া উচিত
  • Original cause, meaningful type, এবং safe context strong exception design-এর foundation

Next Lesson

পরবর্তী lesson:

Validation and Error-Handling Strategies

আমরা শিখব:

  • Input validation layers
  • Constructor validation
  • Domain validation
  • Request validation
  • Fail-fast validation
  • Collecting multiple validation errors
  • Exceptions vs validation results
  • Boundary translation
  • Avoiding duplicated validation
  • Building consistent error responses