Web Development /JavaScript Programming / Lesson
Web Development

Error Handling, Debugging and Defensive Programming

Learn how to identify, handle and prevent JavaScript errors using try/catch, throw, finally, browser developer tools, breakpoints, console debugging, validation and defensive programming techniques.

🎯

Learning Objectives

["Understand common types of JavaScript errors.","Distinguish between syntax errors, runtime errors and logical errors.","Use the browser developer console to investigate problems.","Use console.log(), console.warn() and console.error() appropriately.","Understand how stack traces help locate errors.","Use try\/catch to handle exceptions.","Use finally for cleanup operations.","Use throw to create meaningful application errors.","Create custom Error objects with useful messages.","Handle errors in asynchronous code using async\/await.","Handle rejected Promises safely.","Use breakpoints and browser debugging tools.","Inspect variables while debugging.","Use defensive programming to validate assumptions.","Handle missing DOM elements and invalid user input safely.","Understand why error messages should help developers without exposing sensitive information.","Apply structured debugging techniques to real-world JavaScript applications."]

📖

Lesson Content

Introduction

No matter how experienced a developer becomes, software errors are unavoidable. The important skill is not simply avoiding every error, but learning how to identify problems, understand why they happen, handle them appropriately and prevent them from causing unnecessary damage to an application.

JavaScript provides several tools for working with errors, while modern browsers provide powerful debugging tools that help developers inspect application behavior.

In this lesson, you will learn about JavaScript errors, debugging techniques, exception handling, defensive programming and practical strategies for building more reliable applications.

1. What Is an Error?

An error is a problem that prevents a program from behaving as expected.

Errors can occur for many reasons:

  • A spelling mistake in code.
  • Invalid JavaScript syntax.
  • Using a variable incorrectly.
  • Calling a function that does not exist.
  • Working with unexpected data.
  • Network requests failing.
  • Users entering invalid information.
  • Assumptions about external systems becoming incorrect.

Professional developers learn to treat errors as useful information. An error message often provides clues about what went wrong and where the problem occurred.

2. Syntax Errors

A syntax error occurs when JavaScript code does not follow the language’s syntax rules.

For example:

const name = ;

The JavaScript engine cannot correctly interpret this statement.

Another example:

if (userLoggedIn {
    console.log('Welcome');
}

The missing closing parenthesis causes invalid syntax.

Syntax errors are usually detected before the affected code can execute.

3. Runtime Errors

A runtime error occurs while JavaScript is executing.

For example:

const student = null;

console.log(student.name);

The program attempts to access a property on null, resulting in an error.

Runtime errors can be more difficult to identify because the application may successfully start before the problem occurs.

4. Logical Errors

A logical error occurs when the code runs without producing a JavaScript error, but the result is incorrect.

For example:

function calculateAverage(total, count) {
    return total * count;
}

The function is valid JavaScript, but multiplication is not the correct operation for calculating an average.

Logical errors are often discovered through testing and careful examination of application behavior.

5. Common JavaScript Error Types

JavaScript includes several built-in error types.

  • SyntaxError – invalid JavaScript syntax.
  • ReferenceError – a variable or identifier cannot be found.
  • TypeError – an operation is performed on an inappropriate value.
  • RangeError – a value is outside an allowed range.
  • URIError – an invalid URI operation occurs.
  • Error – a general error object.

6. ReferenceError

A ReferenceError commonly occurs when JavaScript attempts to access an identifier that does not exist.

console.log(studentName);

If studentName has not been declared, the browser can report a ReferenceError.

Check variable spelling and scope when investigating this type of problem.

7. TypeError

A TypeError occurs when an operation is not valid for the value being used.

const student = null;

console.log(student.name);

Another example:

const name = 'John';

name.toFixed(2);

The toFixed() method belongs to numbers, not strings.

8. The Browser Console

Modern browsers provide developer tools that include a JavaScript console.

The console can display:

  • Error messages.
  • Warnings.
  • Developer output.
  • Network-related information.
  • Stack traces.
  • Values inspected during development.

Developers commonly open browser developer tools when investigating frontend problems.

9. console.log()

console.log() can be used to inspect values during development.

const student = {
    name: 'John',
    course: 'Web Development'
};

console.log(student);

You can also inspect individual properties:

console.log(student.name);
console.log(student.course);

Logging can help determine whether a variable contains the value you expect.

10. console.warn()

console.warn() is useful for development warnings.

console.warn('Course data is empty.');

Warnings can communicate situations that are not necessarily fatal but deserve attention.

11. console.error()

console.error() can be used when reporting an error condition.

console.error('Unable to load student data.');

When handling an actual Error object:

try {
    // Operation
} catch (error) {
    console.error(error);
}

12. Understanding Stack Traces

When JavaScript produces an error, the browser may display a stack trace.

A stack trace provides information about the sequence of function calls that led to the error.

For example, if:

function first() {
    second();
}

function second() {
    third();
}

function third() {
    throw new Error('Something went wrong');
}

first();

The stack trace can help show that first() called second(), which called third(), where the error occurred.

13. try/catch

The try/catch statement allows JavaScript to handle exceptions.

try {

    const result = riskyOperation();

} catch (error) {

    console.error('Operation failed:', error);
}

Code inside try is executed normally. If an exception occurs, execution moves to the catch block.

14. The catch Error Object

The caught error is normally an Error object or another thrown value.

Common Error properties include:

  • name – the error type.
  • message – a description of the problem.
  • stack – debugging information about where the error occurred.

Example:

try {

    throw new Error('Student record not found');

} catch (error) {

    console.log(error.name);
    console.log(error.message);
    console.log(error.stack);
}

15. finally

The finally block runs after the try/catch process.

try {

    console.log('Starting operation');

} catch (error) {

    console.error(error);

} finally {

    console.log('Operation finished');
}

finally is useful for cleanup operations that should happen whether an operation succeeds or fails.

16. Throwing Errors

JavaScript allows developers to create errors intentionally with throw.

throw new Error('Invalid student record');

This immediately interrupts normal execution and sends the error to the nearest compatible catch block.

17. Validating Function Input

Throwing errors can be useful when a function receives invalid input.

function calculateProgress(completed, total) {

    if (total <= 0) {
        throw new Error('Total lessons must be greater than zero');
    }

    return (completed / total) * 100;
}

The calling code can decide how to handle the error.

try {

    const progress = calculateProgress(5, 0);

    console.log(progress);

} catch (error) {

    console.error(error.message);
}

18. Custom Error Messages

Good error messages provide useful context.

Instead of:

throw new Error('Error');

prefer something more informative:

throw new Error(
    'Unable to save the student assignment.'
);

During development, precise messages can significantly reduce debugging time.

19. Creating Custom Error Classes

For larger applications, developers can create specialized error classes.

class ValidationError extends Error {

    constructor(message) {

        super(message);

        this.name = 'ValidationError';
    }
}

throw new ValidationError(
    'Student email is required.'
);

The calling code can inspect the error type when necessary.

20. Handling Different Error Types

Applications can respond differently to different kinds of errors.

try {

    performOperation();

} catch (error) {

    if (error instanceof ValidationError) {

        console.warn('Validation failed:', error.message);

    } else {

        console.error('Unexpected error:', error);
    }
}

This allows applications to distinguish expected validation problems from unexpected programming failures.

21. Errors in Asynchronous Code

Errors can also occur in asynchronous JavaScript.

With async/await, try/catch provides a convenient way to handle rejected Promises.

async function loadCourses() {

    try {

        const response = await fetch('/api/courses');

        if (!response.ok) {
            throw new Error(
                `HTTP error: ${response.status}`
            );
        }

        const courses = await response.json();

        return courses;

    } catch (error) {

        console.error(
            'Unable to load courses:',
            error
        );

        return [];
    }
}

22. Handling Promise Rejections

Promises can also be handled with catch().

fetch('/api/courses')
    .then(response => response.json())
    .then(courses => {
        console.log(courses);
    })
    .catch(error => {
        console.error(
            'Request failed:',
            error
        );
    });

Both Promise chaining and async/await can be used. Choose the approach that keeps the application readable and maintainable.

23. Fetch Does Not Automatically Reject on HTTP Errors

One important Fetch API concept is that fetch() normally rejects its Promise for network-level failures, but an HTTP response such as 404 or 500 can still produce a fulfilled Promise.

Therefore, check response.ok when you need to treat unsuccessful HTTP responses as errors.

const response = await fetch('/api/courses');

if (!response.ok) {
    throw new Error(
        `Server returned ${response.status}`
    );
}

24. Debugging with Breakpoints

A breakpoint pauses JavaScript execution at a chosen line.

When execution is paused, developers can inspect:

  • Current variable values.
  • Function parameters.
  • Call stack information.
  • Application state.
  • The next statements that will execute.

Breakpoints are often more useful than adding many console.log() statements when investigating complex problems.

25. Step Over

When debugging, Step Over executes the current line and moves to the next line without entering a function’s internal code.

This is useful when you trust the function being called and want to continue examining the surrounding logic.

26. Step Into

Step Into enters the function being called so you can inspect its internal execution.

This is useful when the suspected problem may be inside that function.

27. Step Out

Step Out finishes the current function and returns execution to the code that called it.

These debugging controls allow developers to examine an application one execution step at a time.

28. Conditional Breakpoints

A conditional breakpoint pauses execution only when a specified condition is true.

For example, if an array contains hundreds of students, you may want to pause only when:

student.id === 100

This avoids stopping the application unnecessarily for every iteration.

29. Debugging DOM Problems

Many frontend JavaScript errors involve DOM elements.

For example:

const button =
    document.querySelector('#submit-button');

button.addEventListener('click', submitForm);

If the element does not exist, button may be null.

A defensive approach can check the element first:

const button =
    document.querySelector('#submit-button');

if (button) {

    button.addEventListener(
        'click',
        submitForm
    );
}

30. Defensive Programming

Defensive programming means writing code that anticipates unexpected conditions rather than assuming everything will always work perfectly.

Examples include:

  • Checking whether required elements exist.
  • Validating user input.
  • Checking API responses.
  • Handling empty arrays.
  • Checking for null or undefined values.
  • Providing fallback values.
  • Handling network failures.
  • Validating function arguments.

31. Optional Chaining

Optional chaining can help safely access nested properties when an intermediate value may not exist.

const city = student?.address?.city;

If student or address is nullish, the expression returns undefined instead of throwing an error.

Optional chaining should be used thoughtfully. It should not replace validation when the application actually requires the value to exist.

32. Nullish Coalescing

The nullish coalescing operator can provide a fallback when a value is null or undefined.

const name = student.name ?? 'Unknown Student';

This is useful when displaying optional data.

33. Validating User Input

User input should never be assumed to be correct.

For example:

function registerStudent(name, email) {

    if (!name || !email) {
        throw new Error(
            'Name and email are required.'
        );
    }

    // Continue registration.
}

More complete applications can also validate formats, lengths and other requirements.

34. Handling Empty API Responses

External data may be empty even when a request succeeds.

const students = await response.json();

if (!Array.isArray(students) || students.length === 0) {

    console.warn('No students were returned.');

    return;
}

This prevents the interface from assuming that records always exist.

35. Debugging with Meaningful Logs

Logs are more useful when they contain context.

Instead of:

console.log(data);

consider:

console.log(
    'Loaded student courses:',
    data
);

Context makes it easier to understand what a logged value represents.

36. Grouping Console Information

Developers can group related debugging output.

console.group('Student Dashboard');

console.log('Student:', student);
console.log('Courses:', courses);
console.log('Progress:', progress);

console.groupEnd();

This can make complex development logs easier to read.

37. Debugging with Assertions

console.assert() can help identify assumptions that are unexpectedly false.

console.assert(
    students.length > 0,
    'Expected at least one student.'
);

Assertions can be useful during development when verifying application assumptions.

38. Avoiding Overuse of try/catch

try/catch is useful, but wrapping every line of code in try/catch can make an application harder to understand.

Handle errors where you can meaningfully respond to them.

For example, a network request function can throw an error while the UI layer decides how to display the error to the user.

39. User-Friendly Error Messages

Technical error details are often useful for developers but confusing to users.

A user interface might display:

Unable to load your courses.
Please try again.

while the developer console contains more technical information:

console.error(
    'Course API request failed:',
    error
);

This separation improves the user experience while preserving useful debugging information.

40. Do Not Expose Sensitive Information

Error messages should not reveal private credentials, passwords, authentication tokens or other sensitive information.

Developers should carefully decide which details are shown to users and which are kept in secure development or server-side logs.

41. Debugging Network Requests

Browser developer tools usually provide a Network panel where developers can inspect requests.

When investigating an API problem, examine:

  • Request URL.
  • HTTP method.
  • Status code.
  • Request headers.
  • Response headers.
  • Response body.
  • Request timing.

This is particularly useful when debugging Fetch API operations.

42. A Structured Debugging Process

When something goes wrong, avoid randomly changing code.

Use a structured process:

  1. Reproduce the problem.
  2. Read the error message carefully.
  3. Identify the file and line involved.
  4. Inspect the relevant variables.
  5. Check assumptions about the input.
  6. Use a breakpoint or targeted log.
  7. Identify the root cause.
  8. Make a focused correction.
  9. Test the correction.
  10. Test related functionality to ensure nothing else broke.

43. Debugging Example

Suppose this function returns an unexpected result:

function calculateProgress(completed, total) {

    return completed / total * 100;
}

console.log(
    calculateProgress(5, 10)
);

The result appears correct. But if total is zero:

console.log(
    calculateProgress(5, 0)
);

The function produces an invalid result for the application’s requirements.

A defensive version could be:

function calculateProgress(completed, total) {

    if (
        !Number.isFinite(completed) ||
        !Number.isFinite(total) ||
        total <= 0
    ) {
        throw new Error(
            'Invalid progress values.'
        );
    }

    return (completed / total) * 100;
}

44. Testing After Fixing an Error

Fixing one case does not always mean the feature is completely correct.

After changing code, test:

  • The original failing case.
  • Normal expected input.
  • Empty input.
  • Unexpected input.
  • Boundary values.
  • Related features.

This helps reduce the possibility of introducing a new problem while fixing the original one.

45. Error Handling in an LMS

A learning management system can encounter many error conditions.

  • Student authentication failure.
  • Course data unavailable.
  • Lesson not found.
  • Assignment submission failure.
  • Invalid form information.
  • Network interruption.
  • Server errors.
  • Missing dashboard elements.

Good error handling allows the LMS to respond gracefully rather than leaving the user with a broken interface.

46. Practical Dashboard Error Handling

async function loadDashboard() {

    const dashboard =
        document.querySelector('#dashboard');

    if (!dashboard) {
        console.error(
            'Dashboard container not found.'
        );
        return;
    }

    dashboard.textContent = 'Loading dashboard...';

    try {

        const response =
            await fetch('/api/dashboard');

        if (!response.ok) {
            throw new Error(
                `HTTP ${response.status}`
            );
        }

        const data =
            await response.json();

        dashboard.textContent =
            `Welcome, ${data.student.name}`;

    } catch (error) {

        console.error(
            'Dashboard loading failed:',
            error
        );

        dashboard.textContent =
            'Unable to load your dashboard.';

    } finally {

        console.log(
            'Dashboard request completed.'
        );
    }
}

47. Error Handling Principles

Good error handling should:

  • Detect problems early.
  • Provide useful information to developers.
  • Provide understandable feedback to users.
  • Prevent unnecessary application crashes.
  • Avoid exposing sensitive information.
  • Allow the application to recover when possible.
  • Keep error-handling logic understandable.

48. Practical Exercise

Create a JavaScript function called calculateGrade(score).

The function should:

  1. Accept a numeric score.
  2. Reject values below 0.
  3. Reject values above 100.
  4. Reject non-numeric values.
  5. Throw a meaningful Error when the input is invalid.
  6. Return a grade when the input is valid.
  7. Use try/catch when calling the function.

49. Advanced Practical Exercise

Build a small course dashboard that loads course data using Fetch API.

The application should:

  • Display a loading message.
  • Check whether the API response is successful.
  • Parse the JSON response.
  • Handle an empty response.
  • Display the courses.
  • Display a friendly error message when loading fails.
  • Log technical details to the browser console.
  • Use try/catch/finally.
  • Check that the required DOM container exists.

50. Debugging Challenge

Find and correct the problems in this code:

async function loadStudent() {

    try {

        const response = fetch('/api/student');

        const student = response.json();

        document.querySelector('#name').textContent =
            student.name;

    } catch (error) {

        console.log('Failed');

    }
}

Identify at least four problems and rewrite the function correctly.

51. Key Takeaways

  • Syntax errors involve invalid JavaScript syntax.
  • Runtime errors occur while code is executing.
  • Logical errors produce incorrect results without necessarily throwing an error.
  • ReferenceError and TypeError are common JavaScript errors.
  • The browser developer console is an important debugging tool.
  • Stack traces help identify where errors occurred.
  • try/catch can handle exceptions.
  • finally is useful for cleanup operations.
  • throw can create meaningful application errors.
  • async/await errors can be handled with try/catch.
  • fetch() should be checked with response.ok when HTTP errors matter.
  • Breakpoints allow developers to inspect execution step by step.
  • Defensive programming anticipates unexpected conditions.
  • User input and external data should be validated.
  • Error messages should provide useful information without exposing sensitive data.
  • Good debugging focuses on identifying the root cause rather than randomly changing code.

Conclusion

Error handling and debugging are essential skills for every JavaScript developer. Applications interact with users, browsers, servers and external services, so unexpected situations are inevitable.

By understanding error types, using developer tools, applying try/catch, creating meaningful errors and practicing defensive programming, developers can build applications that are easier to diagnose, maintain and improve.

These techniques become especially important as JavaScript projects grow into complex systems such as dashboards, LMS platforms, business applications and interactive WordPress websites.

💻

Code Example

async function loadDashboard() {

    const dashboard =
        document.querySelector('#dashboard');

    if (!dashboard) {
        console.error(
            'Dashboard container not found.'
        );
        return;
    }

    dashboard.textContent = 'Loading dashboard...';

    try {

        const response =
            await fetch('/api/dashboard');

        if (!response.ok) {
            throw new Error(
                `HTTP ${response.status}`
            );
        }

        const data =
            await response.json();

        if (!data || !data.student) {
            throw new Error(
                'Invalid dashboard response.'
            );
        }

        dashboard.textContent =
            `Welcome, ${data.student.name}`;

    } catch (error) {

        console.error(
            'Dashboard loading failed:',
            error
        );

        dashboard.textContent =
            'Unable to load your dashboard.';

    } finally {

        console.log(
            'Dashboard request completed.'
        );
    }
}

loadDashboard();
⭐

Key Takeaways

["JavaScript errors can be syntax errors, runtime errors or logical errors.","ReferenceError and TypeError are common runtime error types.","The browser developer console provides valuable debugging information.","Stack traces can show the sequence of function calls that led to an error.","Use try\/catch when code may throw an exception and the application can respond meaningfully.","Use finally for cleanup or completion tasks.","Use throw to create meaningful application errors.","Check response.ok when handling HTTP requests with fetch().","Use breakpoints to inspect application execution and variable values.","Defensive programming validates assumptions before relying on data or DOM elements.","Validate user input and external data.","Optional chaining can safely access potentially missing nested properties.","Use clear user-facing messages while keeping useful technical details available to developers.","Never expose passwords, authentication tokens or other sensitive information through error messages.","A structured debugging process is more effective than randomly changing code."]

Enjoying this lesson?

Sign in to your Dwallnet Academy student account to track your learning progress and mark lessons as complete.

Sign In to Track Progress