Xử lý Lỗi JavaScript - Bí quyết Debug siêu đẳng

Khám phá bí quyết xử lý lỗi JavaScript và trở thành bậc thầy debug

🚨 Xử lý Lỗi trong JavaScript

Chào các bạn! Hôm nay tôi muốn chia sẻ với các bạn về cách xử lý lỗi trong JavaScript - một kỹ năng quan trọng giúp tạo ra ứng dụng ổn định và chuyên nghiệp.

🔍 Lỗi - Tại sao cần xử lý?

Lỗi là điều không thể tránh khỏi trong lập trình. Hiểu cách xử lý lỗi giúp bạn tạo ra ứng dụng ổn định, dễ bảo trì và mang lại trải nghiệm tốt cho người dùng.

Tầm quan trọng của Error Handling: Xử lý lỗi đúng cách giúp ứng dụng ổn định, không crash và cung cấp feedback hữu ích cho user. Trong production, unhandled errors có thể làm crash toàn bộ ứng dụng.

🔍 Các loại lỗi phổ biến

1. Syntax Error (Lỗi cú pháp)

// Lỗi cú pháp - thiếu dấu ngoặc
function greet(name {
    return `Xin chào ${name}!`;
}

// Lỗi cú pháp - thiếu dấu chấm phẩy
let message = "Hello"
console.log(message)

// Lỗi cú pháp - thiếu dấu ngoặc kép
let text = 'Xin chào JavaScript!;

Ở trên, mình cố tình tạo 3 lỗi cơ bản rất hay gặp khi mới học:

  • Thiếu dấu ngoặc tròn ) khi khai báo hàm → trình thông dịch dừng ngay ở bước parse.
  • Quên dấu chấm phẩy trong môi trường build nghiêm ngặt có thể gây lỗi ngoài ý muốn.
  • Thiếu dấu nháy kết thúc chuỗi khiến phần còn lại bị hiểu sai là string.

Cách mình sửa nhanh:

  • Đọc kỹ thông báo lỗi, chú ý cột/ký tự mà IDE highlight.
  • Dùng formatter (Prettier) để so khớp cặp ngoặc/nháy.
  • Bật tính năng tô màu cặp ngoặc để dễ soi sai lệch.

2. Reference Error (Lỗi tham chiếu)

// Biến chưa được khai báo
console.log(undefinedVariable); // ReferenceError

// Hàm chưa được định nghĩa
nonExistentFunction(); // ReferenceError

// Truy cập thuộc tính của undefined
let obj;
console.log(obj.property); // TypeError

Lỗi này xuất hiện khi gọi biến/hàm chưa khai báo hoặc truy cập dữ liệu chưa khởi tạo. Mẹo nhỏ:

  • Ưu tiên const/let và để scope hẹp để IDE gợi ý, bắt lỗi sớm.
  • Dùng optional chaining: obj?.property khi dữ liệu có thể rỗng.

3. Type Error (Lỗi kiểu dữ liệu)

// Gọi hàm trên biến không phải hàm
let notAFunction = "hello";
notAFunction(); // TypeError

// Thay đổi thuộc tính readonly
let obj = Object.freeze({name: "TG-Bao"});
obj.name = "Alice"; // TypeError

// Truy cập thuộc tính của null
let nullVar = null;
console.log(nullVar.property); // TypeError

TypeError xảy ra khi “kỳ vọng” về kiểu không khớp. Thêm guard trước khi gọi:

if (typeof maybeFn === 'function') {
  maybeFn();
}

Ở module phức tạp, cân nhắc dùng TypeScript để kiểm tra kiểu tĩnh.

4. Runtime Error (Lỗi thời gian chạy)

// Chia cho 0
let result = 10 / 0; // Infinity (không phải lỗi)
let result2 = 0 / 0;  // NaN

// Truy cập mảng ngoài phạm vi
let arr = [1, 2, 3];
console.log(arr[10]); // undefined (không phải lỗi)

// Gọi phương thức không tồn tại
let str = "hello";
str.nonExistentMethod(); // TypeError

Runtime thường do dữ liệu thực tế khác kỳ vọng. Mình xử lý bằng cách:

  • Viết unit test cho các nhánh xử lý quan trọng.
  • Thêm fallback hợp lý (trả về giá trị mặc định, hiển thị thông báo thân thiện).

🛡️ Xử lý lỗi với Try-Catch

1. Try-Catch cơ bản

try {
    // Code có thể gây lỗi
    let result = riskyOperation();
    console.log("Kết quả:", result);
} catch (error) {
    // Xử lý lỗi
    console.error("Đã xảy ra lỗi:", error.message);
} finally {
    // Luôn thực thi
    console.log("Hoàn thành xử lý");
}

function riskyOperation() {
    if (Math.random() > 0.5) {
        throw new Error("Lỗi ngẫu nhiên!");
    }
    return "Thành công!";
}

Chỉ bọc try/catch ở rìa ngoài (boundary) nơi có thể hiển thị thông báo và ghi log. Tránh lạm dụng ở mọi nơi vì có thể che mất bug thật.

2. Try-Catch với nhiều loại lỗi

function processData(data) {
    try {
        // Kiểm tra dữ liệu đầu vào
        if (!data) {
            throw new Error("Dữ liệu không được để trống");
        }
        
        if (typeof data !== 'object') {
            throw new TypeError("Dữ liệu phải là object");
        }
        
        if (!data.name) {
            throw new Error("Thiếu thuộc tính 'name'");
        }
        
        // Xử lý dữ liệu
        return `Xin chào ${data.name}!`;
        
    } catch (error) {
        if (error instanceof TypeError) {
            console.error("Lỗi kiểu dữ liệu:", error.message);
        } else if (error instanceof Error) {
            console.error("Lỗi chung:", error.message);
        } else {
            console.error("Lỗi không xác định:", error);
        }
        
        // Trả về giá trị mặc định
        return "Xử lý dữ liệu thất bại";
    }
}

// Test các trường hợp
console.log(processData(null));
console.log(processData("string"));
console.log(processData({}));
console.log(processData({name: "TG-Bao"}));

3. Async/Await với Try-Catch

async function fetchUserData(userId) {
    try {
        const response = await fetch(`/api/users/${userId}`);
        
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        const userData = await response.json();
        return userData;
        
    } catch (error) {
        console.error("Lỗi khi tải dữ liệu người dùng:", error.message);
        return null;
    }
}

// Sử dụng
fetchUserData(123).then(userData => {
    if (userData) {
        console.log("Dữ liệu người dùng:", userData);
    } else {
        console.log("Không thể tải dữ liệu người dùng");
    }
});

Khi gọi API, luôn kiểm tra response.ok, chuẩn hóa lỗi, và ở UI hiển thị thông điệp dễ hiểu kèm hành động tiếp theo cho người dùng.

🔧 Debugging Techniques

1. Console Methods

// Console.log - In thông tin
console.log("Thông tin chung:", data);

// Console.error - In lỗi
console.error("Đã xảy ra lỗi:", error);

// Console.warn - Cảnh báo
console.warn("Cảnh báo: Dữ liệu có thể không chính xác");

// Console.info - Thông tin
console.info("Thông tin: Quá trình đang chạy");

// Console.debug - Debug
console.debug("Debug: Giá trị biến:", variable);

// Console.table - Hiển thị dạng bảng
let users = [
    {name: "TG-Bao", age: 25},
    {name: "Alice", age: 30}
];
console.table(users);

// Console.group - Nhóm thông tin
console.group("Thông tin người dùng");
console.log("Tên:", user.name);
console.log("Tuổi:", user.age);
console.groupEnd();

2. Breakpoints và Debugger

function complexFunction(data) {
    // Đặt breakpoint tại đây
    debugger;
    
    let result = 0;
    
    for (let i = 0; i < data.length; i++) {
        // Breakpoint có điều kiện
        if (data[i] > 100) {
            debugger; // Chỉ dừng khi điều kiện đúng
        }
        
        result += data[i];
    }
    
    return result;
}

// Sử dụng trong browser DevTools
let numbers = [10, 20, 150, 30];
let sum = complexFunction(numbers);

Debugger giúp quan sát luồng dữ liệu. Hãy dùng breakpoint có điều kiện để dừng đúng lúc, tiết kiệm thời gian.

3. Error Logging

// Hàm ghi log lỗi
function logError(error, context = {}) {
    const errorLog = {
        timestamp: new Date().toISOString(),
        message: error.message,
        stack: error.stack,
        context: context,
        userAgent: navigator.userAgent,
        url: window.location.href
    };
    
    // Gửi lỗi đến server
    fetch('/api/errors', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(errorLog)
    }).catch(err => {
        console.error('Không thể gửi log lỗi:', err);
    });
    
    // Lưu vào localStorage
    try {
        let errorHistory = JSON.parse(localStorage.getItem('errorHistory') || '[]');
        errorHistory.push(errorLog);
        localStorage.setItem('errorHistory', JSON.stringify(errorHistory));
    } catch (e) {
        console.error('Không thể lưu lỗi vào localStorage:', e);
    }
}

// Sử dụng
try {
    riskyOperation();
} catch (error) {
    logError(error, {operation: 'riskyOperation', userId: 123});
}

Ghi log lỗi giúp tái hiện vấn đề. Ở production có thể gửi về server hoặc dịch vụ như Sentry; lưu ý không kèm thông tin nhạy cảm (PII).

🎯 Ví dụ thực tế: Form Validation

<!DOCTYPE html>
<html>
<head>
    <title>Form Validation với Error Handling</title>
    <style>
        .form-container {
            max-width: 500px;
            margin: 50px auto;
            padding: 20px;
            border: 2px solid #ccc;
            border-radius: 10px;
        }
        .form-group {
            margin-bottom: 15px;
        }
        label {
            display: block;
            margin-bottom: 5px;
            font-weight: bold;
        }
        input, select {
            width: 100%;
            padding: 8px;
            border: 1px solid #ccc;
            border-radius: 4px;
        }
        .error {
            color: red;
            font-size: 14px;
            margin-top: 5px;
        }
        .success {
            color: green;
            font-size: 14px;
            margin-top: 5px;
        }
        button {
            background-color: #007bff;
            color: white;
            padding: 10px 20px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        button:hover {
            background-color: #0056b3;
        }
    </style>
</head>
<body>
    <div class="form-container">
        <h2>Đăng ký tài khoản</h2>
        <form id="registrationForm">
            <div class="form-group">
                <label for="name">Họ và tên:</label>
                <input type="text" id="name" name="name" required>
                <div id="nameError" class="error"></div>
            </div>
            
            <div class="form-group">
                <label for="email">Email:</label>
                <input type="email" id="email" name="email" required>
                <div id="emailError" class="error"></div>
            </div>
            
            <div class="form-group">
                <label for="age">Tuổi:</label>
                <input type="number" id="age" name="age" required>
                <div id="ageError" class="error"></div>
            </div>
            
            <div class="form-group">
                <label for="password">Mật khẩu:</label>
                <input type="password" id="password" name="password" required>
                <div id="passwordError" class="error"></div>
            </div>
            
            <div class="form-group">
                <label for="confirmPassword">Xác nhận mật khẩu:</label>
                <input type="password" id="confirmPassword" name="confirmPassword" required>
                <div id="confirmPasswordError" class="error"></div>
            </div>
            
            <button type="submit">Đăng ký</button>
        </form>
        
        <div id="successMessage" class="success"></div>
    </div>

    <script>
        // Hàm validation
        class FormValidator {
            constructor() {
                this.errors = {};
            }
            
            // Validate tên
            validateName(name) {
                if (!name || name.trim().length === 0) {
                    return "Tên không được để trống";
                }
                if (name.trim().length < 2) {
                    return "Tên phải có ít nhất 2 ký tự";
                }
                if (!/^[a-zA-ZÀ-ỹ\s]+$/.test(name.trim())) {
                    return "Tên chỉ được chứa chữ cái và khoảng trắng";
                }
                return null;
            }
            
            // Validate email
            validateEmail(email) {
                if (!email || email.trim().length === 0) {
                    return "Email không được để trống";
                }
                const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
                if (!emailRegex.test(email.trim())) {
                    return "Email không hợp lệ";
                }
                return null;
            }
            
            // Validate tuổi
            validateAge(age) {
                if (!age || age.toString().trim().length === 0) {
                    return "Tuổi không được để trống";
                }
                const ageNum = parseInt(age);
                if (isNaN(ageNum)) {
                    return "Tuổi phải là số";
                }
                if (ageNum < 18) {
                    return "Bạn phải ít nhất 18 tuổi";
                }
                if (ageNum > 100) {
                    return "Tuổi không hợp lệ";
                }
                return null;
            }
            
            // Validate mật khẩu
            validatePassword(password) {
                if (!password || password.length === 0) {
                    return "Mật khẩu không được để trống";
                }
                if (password.length < 8) {
                    return "Mật khẩu phải có ít nhất 8 ký tự";
                }
                if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(password)) {
                    return "Mật khẩu phải chứa ít nhất 1 chữ hoa, 1 chữ thường và 1 số";
                }
                return null;
            }
            
            // Validate xác nhận mật khẩu
            validateConfirmPassword(password, confirmPassword) {
                if (password !== confirmPassword) {
                    return "Mật khẩu xác nhận không khớp";
                }
                return null;
            }
            
            // Validate toàn bộ form
            validateForm(formData) {
                this.errors = {};
                
                const nameError = this.validateName(formData.name);
                if (nameError) this.errors.name = nameError;
                
                const emailError = this.validateEmail(formData.email);
                if (emailError) this.errors.email = emailError;
                
                const ageError = this.validateAge(formData.age);
                if (ageError) this.errors.age = ageError;
                
                const passwordError = this.validatePassword(formData.password);
                if (passwordError) this.errors.password = passwordError;
                
                const confirmPasswordError = this.validateConfirmPassword(
                    formData.password, 
                    formData.confirmPassword
                );
                if (confirmPasswordError) this.errors.confirmPassword = confirmPasswordError;
                
                return Object.keys(this.errors).length === 0;
            }
        }
        
        // Xử lý form
        document.getElementById('registrationForm').addEventListener('submit', function(e) {
            e.preventDefault();
            
            try {
                // Lấy dữ liệu form
                const formData = {
                    name: document.getElementById('name').value,
                    email: document.getElementById('email').value,
                    age: document.getElementById('age').value,
                    password: document.getElementById('password').value,
                    confirmPassword: document.getElementById('confirmPassword').value
                };
                
                // Validate form
                const validator = new FormValidator();
                const isValid = validator.validateForm(formData);
                
                // Xóa thông báo lỗi cũ
                document.querySelectorAll('.error').forEach(el => el.textContent = '');
                document.getElementById('successMessage').textContent = '';
                
                if (isValid) {
                    // Form hợp lệ
                    document.getElementById('successMessage').textContent = 'Đăng ký thành công!';
                    console.log('Form data:', formData);
                    
                    // Reset form
                    this.reset();
                } else {
                    // Hiển thị lỗi
                    Object.keys(validator.errors).forEach(field => {
                        const errorElement = document.getElementById(field + 'Error');
                        if (errorElement) {
                            errorElement.textContent = validator.errors[field];
                        }
                    });
                }
                
            } catch (error) {
                console.error('Lỗi xử lý form:', error);
                alert('Đã xảy ra lỗi khi xử lý form. Vui lòng thử lại.');
            }
        });
    </script>
</body>
</html>

🎓 Best Practices

1. Error Boundaries

// Hàm wrapper để bắt lỗi
function safeExecute(fn, context = {}) {
    try {
        return fn();
    } catch (error) {
        console.error('Lỗi trong safeExecute:', error);
        logError(error, context);
        return null;
    }
}

// Sử dụng
const result = safeExecute(() => {
    return riskyOperation();
}, {operation: 'riskyOperation'});

2. Graceful Degradation

// Kiểm tra tính năng trước khi sử dụng
function useFeature() {
    if (typeof localStorage !== 'undefined') {
        // Sử dụng localStorage
        localStorage.setItem('data', 'value');
    } else {
        // Fallback
        console.warn('localStorage không khả dụng');
    }
}

3. Error Recovery

// Thử lại khi gặp lỗi
async function retryOperation(operation, maxRetries = 3) {
    for (let i = 0; i < maxRetries; i++) {
        try {
            return await operation();
        } catch (error) {
            console.warn(`Lần thử ${i + 1} thất bại:`, error.message);
            if (i === maxRetries - 1) {
                throw error;
            }
            // Chờ trước khi thử lại
            await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
        }
    }
}

🎯 Kết luận

Xử lý lỗi và debug là kỹ năng quan trọng trong lập trình. Nó giúp bạn tạo ra ứng dụng ổn định và dễ bảo trì.

Hãy thực hành nhiều để nắm vững các kỹ thuật xử lý lỗi! 🚀

Chatbot