Cách xử lý lỗi (Errors) trong JavaScript

Javascript căn bản | by Học Javascript

Trong quá trình phát triển ứng dụng bằng JavaScript, lỗi (errors) là điều không thể tránh khỏi. Chúng có thể xuất hiện do cú pháp sai, tham chiếu biến không tồn tại, kiểu dữ liệu không hợp lệ, hoặc lỗi khi làm việc với bất đồng bộ. Nếu không xử lý đúng cách, lỗi có thể làm gián đoạn chương trình, gây ra trải nghiệm không tốt cho người dùng và ảnh hưởng đến hiệu suất ứng dụng.

Hiểu cách nhận diện và xử lý lỗi hiệu quả là một kỹ năng quan trọng đối với lập trình viên JavaScript. Bằng cách sử dụng các kỹ thuật như try...catch, throw, và Promise.catch(), chúng ta có thể kiểm soát lỗi, ngăn chặn sự cố và đảm bảo ứng dụng hoạt động mượt mà.

Trong bài viết này, mình sẽ tìm hiểu về các loại lỗi phổ biến trong JavaScript, cách xử lý chúng, cũng như những phương pháp tốt nhất để tránh lỗi và cải thiện chất lượng mã nguồn.

Giới thiệu về lỗi trong JavaScript

Trong JavaScript, lỗi (error) là các vấn đề xảy ra trong quá trình thực thi mã, khiến chương trình không chạy đúng như mong đợi. Lỗi có thể phát sinh do cú pháp sai, sử dụng biến chưa được khai báo, gọi hàm không tồn tại, hoặc thực hiện các thao tác bất hợp lệ trên dữ liệu.

Ví dụ, nếu cố gắng truy cập một biến chưa được khai báo, JavaScript sẽ ném ra một lỗi:

console.log(myVariable); // ReferenceError: myVariable is not defined

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

Việc xử lý lỗi trong JavaScript rất quan trọng vì:

  • Tránh làm gián đoạn chương trình: Nếu không xử lý lỗi, ứng dụng có thể bị dừng đột ngột.
  • Cải thiện trải nghiệm người dùng: Thay vì hiển thị lỗi kỹ thuật, chúng ta có thể đưa ra thông báo dễ hiểu hơn.
  • Dễ dàng gỡ lỗi và bảo trì: Xử lý lỗi tốt giúp phát hiện vấn đề nhanh chóng và sửa lỗi dễ dàng.
  • Bảo mật ứng dụng: Một số lỗi có thể làm lộ thông tin nhạy cảm nếu không được kiểm soát đúng cách.

Ví dụ, nếu một API không phản hồi đúng, ứng dụng có thể bị treo nếu không có xử lý lỗi thích hợp:

fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error("Lỗi khi gọi API:", error));

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

Dưới đây là một số loại lỗi thường gặp trong JavaScript:

SyntaxError (Lỗi cú pháp)

  • Xảy ra khi mã nguồn không tuân theo cú pháp JavaScript hợp lệ.
  • Ví dụ:
console.log("Hello World" // Lỗi: thiếu dấu đóng ngoặc

ReferenceError (Lỗi tham chiếu)

  • Xảy ra khi cố gắng sử dụng biến hoặc hàm chưa được khai báo.
  • Ví dụ:
console.log(nonExistentVar); // ReferenceError: nonExistentVar is not defined

TypeError (Lỗi kiểu dữ liệu)

  • Xảy ra khi thực hiện một thao tác trên kiểu dữ liệu không phù hợp.
  • Ví dụ:
let num = 10;
num(); // TypeError: num is not a function

RangeError (Lỗi phạm vi)

  • Xảy ra khi một giá trị vượt quá phạm vi hợp lệ, chẳng hạn như lặp vô hạn hoặc sử dụng mảng quá lớn.
  • Ví dụ:
function recursive() {
    return recursive();
}
recursive(); // RangeError: Maximum call stack size exceeded
  • EvalError (Lỗi eval())

    • Xảy ra khi sử dụng eval() không hợp lệ. (Tuy nhiên, lỗi này hiếm gặp vì eval() ít được sử dụng).
  • URIError (Lỗi URI không hợp lệ)

    • Xảy ra khi sử dụng các hàm mã hóa/giải mã URI không đúng cách.
    • Ví dụ:
decodeURIComponent('%'); // URIError: URI malformed

Các loại lỗi trong JavaScript

Trong quá trình lập trình JavaScript, chúng ta có thể gặp phải nhiều loại lỗi khác nhau. Việc hiểu rõ từng loại lỗi giúp phát hiện và xử lý vấn đề nhanh chóng, giúp chương trình chạy ổn định hơn. Dưới đây là các loại lỗi phổ biến trong JavaScript và cách nhận diện chúng.

Lỗi cú pháp (SyntaxError)

Lỗi cú pháp xảy ra khi mã JavaScript vi phạm các quy tắc cú pháp của ngôn ngữ. Đây là lỗi thường gặp nhất, đặc biệt là với người mới học lập trình.

Lỗi do thiếu dấu đóng ngoặc

console.log("Xin chào"; // SyntaxError: Unexpected token ';'

Cách sửa:

console.log("Xin chào");

Lỗi do sử dụng từ khóa không hợp lệ

var let = 10; // SyntaxError: Unexpected token 'let'

Cách sửa: Không được dùng let làm tên biến vì đây là từ khóa trong JavaScript.

var myVar = 10;

Lỗi tham chiếu (ReferenceError)

Lỗi tham chiếu xảy ra khi truy cập một biến hoặc hàm chưa được khai báo hoặc không tồn tại trong phạm vi hiện tại.

Gọi biến chưa được khai báo

console.log(myVar); // ReferenceError: myVar is not defined

Cách sửa:

let myVar = 10;
console.log(myVar);

Gọi hàm chưa khai báo

sayHello(); // ReferenceError: sayHello is not defined

Cách sửa: Định nghĩa hàm trước khi gọi nó.

function sayHello() {
    console.log("Hello!");
}
sayHello();

Lỗi kiểu dữ liệu (TypeError)

Lỗi kiểu dữ liệu xảy ra khi thực hiện một thao tác không hợp lệ trên kiểu dữ liệu không phù hợp.

Gọi một giá trị không phải là hàm

let num = 10;
num(); // TypeError: num is not a function

Cách sửa:

let num = 10;
console.log(num);

Truy cập thuộc tính của null hoặc undefined

let obj = null;
console.log(obj.name); // TypeError: Cannot read properties of null

Cách sửa: Kiểm tra giá trị trước khi truy cập thuộc tính.

let obj = null;
console.log(obj?.name); // undefined (không gây lỗi)

Lỗi phạm vi (RangeError)

Lỗi này xảy ra khi một giá trị vượt quá phạm vi hợp lệ, ví dụ như sử dụng đệ quy vô hạn hoặc truyền giá trị không hợp lệ vào một hàm.

Gọi đệ quy vô hạn

function recursive() {
    return recursive();
}
recursive(); // RangeError: Maximum call stack size exceeded

Cách sửa: Cần có điều kiện dừng trong đệ quy.

function recursive(n) {
    if (n <= 0) return;
    console.log(n);
    return recursive(n - 1);
}
recursive(5);

Tạo một mảng có độ dài không hợp lệ

let arr = new Array(-1); // RangeError: Invalid array length

Cách sửa:

let arr = new Array(5); // Tạo mảng có 5 phần tử

Lỗi đánh giá (EvalError)

Lỗi này xảy ra khi sử dụng eval() không hợp lệ. Tuy nhiên, từ ECMAScript 5, EvalError hiếm khi xuất hiện vì JavaScript không còn sử dụng lỗi này thường xuyên.

try {
    throw new EvalError("Lỗi EvalError xảy ra!");
} catch (e) {
    console.log(e.name + ": " + e.message);
}
// Output: EvalError: Lỗi EvalError xảy ra!

Cách tránh: Tránh sử dụng eval() vì nó có thể gây rủi ro bảo mật và làm chậm chương trình.

Lỗi URI (URIError)

Lỗi này xảy ra khi truyền một chuỗi không hợp lệ vào các hàm xử lý URI như decodeURIComponent()encodeURIComponent().

Giải mã một URI không hợp lệ

decodeURIComponent('%'); // URIError: URI malformed
Cách sửa: Kiểm tra dữ liệu trước khi giải mã.
try {
    console.log(decodeURIComponent('%'));
} catch (e) {
    console.log("Lỗi URI: " + e.message);
}

Cách xử lý lỗi trong JavaScript

Xử lý lỗi là một phần quan trọng trong lập trình JavaScript giúp ngăn chặn chương trình bị gián đoạn và đảm bảo trải nghiệm người dùng tốt hơn. JavaScript cung cấp nhiều cơ chế để bắt và xử lý lỗi hiệu quả. Dưới đây là các phương pháp xử lý lỗi phổ biến trong JavaScript.

Sử dụng try...catch

Cú pháp và cách hoạt động

Cấu trúc try...catch cho phép bắt lỗi trong một khối mã cụ thể mà không làm gián đoạn toàn bộ chương trình. Nếu có lỗi xảy ra trong khối try, khối catch sẽ thực thi và xử lý lỗi.

try {
    // Code có thể gây lỗi
} catch (error) {
    // Xử lý lỗi
}

Ví dụ minh họa

try {
    let result = 10 / x; // Lỗi: x chưa được khai báo
    console.log(result);
} catch (error) {
    console.log("Đã xảy ra lỗi: " + error.message);
}

Output:

Đã xảy ra lỗi: x is not defined

Trong ví dụ này, JavaScript phát hiện x chưa được khai báo và ngay lập tức nhảy vào khối catch để xử lý lỗi thay vì dừng toàn bộ chương trình.

Sử dụng try...catch...finally

Thêm khối finally để luôn thực thi một đoạn mã

Khối finally chứa mã sẽ luôn được thực thi bất kể lỗi có xảy ra hay không. Điều này hữu ích khi bạn cần thực hiện một số thao tác dọn dẹp như đóng kết nối, giải phóng tài nguyên, v.v.

try {
    let result = 10 / 0; // Không gây lỗi nhưng trả về Infinity
    console.log(result);
} catch (error) {
    console.log("Đã xảy ra lỗi: " + error.message);
} finally {
    console.log("Khối finally luôn được thực thi.");
}

Output:

Infinity
Khối finally luôn được thực thi.

Dù không có lỗi, finally vẫn thực thi. Nếu có lỗi, finally vẫn chạy sau catch.

Sử dụng throw để tạo lỗi tùy chỉnh

Cách tự tạo và ném lỗi

Lệnh throw cho phép lập trình viên tạo ra lỗi của riêng họ thay vì chỉ dựa vào lỗi hệ thống.

throw new Error("Đây là lỗi tùy chỉnh");

Ví dụ minh họa

function checkAge(age) {
    if (age < 18) {
        throw new Error("Bạn chưa đủ 18 tuổi!");
    }
    return "Bạn đủ tuổi!";
}

try {
    console.log(checkAge(16)); // Lỗi sẽ xảy ra
} catch (error) {
    console.log("Lỗi: " + error.message);
}

Output:

Lỗi: Bạn chưa đủ 18 tuổi!

Nếu age nhỏ hơn 18, lỗi tùy chỉnh sẽ được ném ra và bắt bởi catch.

Sử dụng catch với tham số lỗi

Trích xuất thông tin lỗi từ đối tượng lỗi

Khi xảy ra lỗi, JavaScript tạo ra một đối tượng lỗi chứa thông tin chi tiết về lỗi. Chúng ta có thể sử dụng các thuộc tính như:

  • message – mô tả lỗi
  • name – loại lỗi (e.g., TypeError, ReferenceError)
  • stack – thông tin về nơi xảy ra lỗi
try {
    let arr = null;
    console.log(arr.length); // Lỗi: Cannot read properties of null
} catch (error) {
    console.log("Tên lỗi: " + error.name);
    console.log("Mô tả lỗi: " + error.message);
}

Output:

Tên lỗi: TypeError
Mô tả lỗi: Cannot read properties of null (reading 'length')

Chúng ta có thể sử dụng thông tin này để hiển thị lỗi cụ thể hơn cho người dùng hoặc log lỗi vào hệ thống.

Sử dụng window.onerror để bắt lỗi toàn cục

window.onerror cho phép bắt tất cả lỗi xảy ra trên trang web, bao gồm lỗi chưa được xử lý trong try...catch. Điều này hữu ích để log lỗi trên server hoặc hiển thị thông báo thân thiện với người dùng.

window.onerror = function(message, source, lineno, colno, error) {
    console.log("Lỗi toàn cục:", message);
    console.log("Tệp tin:", source);
    console.log("Dòng:", lineno, "Cột:", colno);
    return true; // Ngăn chặn lỗi hiển thị trên console
};

// Gây lỗi để kiểm tra
nonExistentFunction();

Output:

Lỗi toàn cục: nonExistentFunction is not defined
Tệp tin: index.js
Dòng: 10 Cột: 1

Lưu ý: window.onerror chỉ hoạt động cho lỗi script chứ không bắt được lỗi trong try...catch.

Sử dụng console.error() để debug lỗi

Cách sử dụng console.error() để ghi lỗi vào console

console.error() giúp ghi lỗi vào console một cách rõ ràng hơn console.log().

try {
    let obj = undefined;
    console.log(obj.name); // Lỗi: Cannot read properties of undefined
} catch (error) {
    console.error("Lỗi xảy ra:", error);
}

Output trong console:

Lỗi xảy ra: TypeError: Cannot read properties of undefined (reading 'name')

Xử lý lỗi bất đồng bộ (Async Error Handling) trong JavaScript

Khi làm việc với JavaScript, nhiều thao tác như gọi API, đọc file hoặc xử lý dữ liệu từ cơ sở dữ liệu đều diễn ra bất đồng bộ. Do đó, việc xử lý lỗi trong các hàm bất đồng bộ (Promise, async/await) là rất quan trọng để đảm bảo chương trình không bị gián đoạn và hoạt động ổn định.

Xử lý lỗi trong Promise với .catch()

Cách dùng .catch() để bắt lỗi trong Promise

Khi một Promise bị từ chối (reject), chúng ta có thể dùng .catch() để bắt lỗi và xử lý nó.

Cú pháp:

myPromise
    .then(result => {
        // Xử lý khi Promise thành công
    })
    .catch(error => {
        // Xử lý khi có lỗi
    });

Ví dụ minh họa

Giả sử chúng ta có một hàm fetchData() mô phỏng việc gọi API, nhưng đôi khi có thể thất bại.

function fetchData() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            let success = Math.random() > 0.5; // 50% cơ hội lỗi
            if (success) {
                resolve("Dữ liệu đã tải thành công!");
            } else {
                reject("Lỗi: Không thể tải dữ liệu!");
            }
        }, 1000);
    });
}

fetchData()
    .then(data => console.log(data))
    .catch(error => console.error(error));

Kết quả có thể là:

Dữ liệu đã tải thành công!

hoặc

Lỗi: Không thể tải dữ liệu!

Xử lý lỗi trong async/await với try...catch

Cách dùng try...catch trong async/await

Khi sử dụng async/await, chúng ta không thể dùng .catch() trực tiếp mà phải sử dụng try...catch để bắt lỗi.

Cú pháp:

async function myFunction() {
    try {
        let result = await asyncOperation();
        console.log(result);
    } catch (error) {
        console.error("Lỗi:", error);
    }
}

Ví dụ minh họa

Dưới đây là cách viết lại fetchData() bằng async/await với xử lý lỗi trong try...catch.

async function fetchDataAsync() {
    try {
        let data = await fetchData();
        console.log("Dữ liệu nhận được:", data);
    } catch (error) {
        console.error("Lỗi xảy ra:", error);
    }
}

fetchDataAsync();

Kết quả có thể là:

Dữ liệu nhận được: Dữ liệu đã tải thành công!

hoặc

Lỗi xảy ra: Không thể tải dữ liệu!

Sử dụng finally với async/await

  • finally chạy dù có lỗi hay không.
  • Dùng để dọn dẹp tài nguyên như đóng kết nối, dừng tải, hiển thị thông báo.

Cú pháp:

async function myFunction() {
    try {
        let result = await asyncOperation();
        console.log(result);
    } catch (error) {
        console.error("Lỗi:", error);
    } finally {
        console.log("Hoàn thành xử lý.");
    }
}

Ví dụ minh họa

Dưới đây là cách dùng finally để đảm bảo thông báo luôn hiển thị dù thành công hay lỗi.

async function fetchDataWithFinally() {
    try {
        let data = await fetchData();
        console.log("Dữ liệu:", data);
    } catch (error) {
        console.error("Lỗi xảy ra:", error);
    } finally {
        console.log("Kết thúc quá trình lấy dữ liệu.");
    }
}

fetchDataWithFinally();

Kết quả có thể là:

Dữ liệu: Dữ liệu đã tải thành công!
Kết thúc quá trình lấy dữ liệu.

hoặc

Lỗi xảy ra: Không thể tải dữ liệu!
Kết thúc quá trình lấy dữ liệu.

Ứng dụng thực tế của xử lý lỗi trong JavaScript

Xử lý lỗi là một phần quan trọng trong việc phát triển ứng dụng JavaScript. Dưới đây là một số tình huống thực tế mà chúng ta cần xử lý lỗi một cách hiệu quả.

Xử lý lỗi khi gọi API

Trong các ứng dụng web, việc gọi API để lấy dữ liệu từ server là rất phổ biến. Tuy nhiên, API có thể gặp lỗi do mất kết nối mạng, server lỗi hoặc dữ liệu phản hồi không đúng định dạng.

Ví dụ minh họa: Xử lý lỗi khi gọi API bằng fetch()

async function fetchUserData(userId) {
    try {
        let response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
        
        if (!response.ok) {
            throw new Error(`Lỗi HTTP: ${response.status}`);
        }

        let data = await response.json();
        console.log("Dữ liệu người dùng:", data);
    } catch (error) {
        console.error("Không thể lấy dữ liệu:", error.message);
    }
}

fetchUserData(1); // Gọi API và xử lý lỗi

Kết quả có thể là:

  • Nếu thành công: Hiển thị thông tin người dùng.
  • Nếu lỗi: Hiển thị thông báo lỗi thay vì làm trang bị treo.

Best Practice:

  • Luôn kiểm tra response.ok trước khi xử lý dữ liệu.
  • Thông báo lỗi rõ ràng khi API bị lỗi.

Xử lý lỗi khi thao tác với dữ liệu người dùng

Người dùng có thể nhập dữ liệu không hợp lệ vào form, như email sai định dạng, mật khẩu yếu hoặc trường bị bỏ trống. Việc xử lý lỗi giúp cải thiện trải nghiệm người dùng và tránh lỗi không mong muốn.

Ví dụ minh họa: Kiểm tra dữ liệu nhập từ form

function validateEmail(email) {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!emailRegex.test(email)) {
        throw new Error("Email không hợp lệ!");
    }
    return true;
}

try {
    validateEmail("test@example"); // Email sai định dạng
} catch (error) {
    console.error(error.message);
}

Kết quả:

Email không hợp lệ!

Các lưu ý và best practices khi xử lý lỗi trong JavaScript

Để xử lý lỗi hiệu quả trong JavaScript, cần tuân thủ một số nguyên tắc sau:

Chỉ bắt lỗi cần thiết, tránh bắt lỗi không mong muốn

try {
    JSON.parse("{ invalid json }"); // Lỗi do JSON không hợp lệ
} catch (error) {
    console.error("Lỗi phân tích JSON:", error.message);
}

Không nên:

try {
    someFunction(); // Lỗi có thể không liên quan đến JSON
} catch (error) {
    console.error("Lỗi JSON:", error.message); // Không chính xác
}

Lưu ý:

  • Luôn chỉ bắt lỗi liên quan, tránh try...catch toàn bộ chương trình.

Luôn cung cấp thông báo lỗi rõ ràng

try {
    throw new Error("Không tìm thấy người dùng");
} catch (error) {
    console.error("Lỗi:", error.message);
}

Hiển thị thông báo cụ thể, dễ hiểu.

Ghi log lỗi để theo dõi và sửa lỗi nhanh hơn

Khi phát triển ứng dụng lớn, nên sử dụng console.error(), logging service để lưu trữ lỗi.

Ví dụ: Gửi lỗi lên server để theo dõi:

function logError(error) {
    fetch('/log-error', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ error: error.message, time: new Date() })
    });
}

try {
    someFunction(); // Hàm có thể lỗi
} catch (error) {
    logError(error);
}

Kết bài

Xử lý lỗi là một kỹ năng quan trọng trong lập trình JavaScript, giúp ứng dụng hoạt động ổn định, tránh các sự cố không mong muốn và nâng cao trải nghiệm người dùng. Việc hiểu rõ các loại lỗi phổ biến như SyntaxError, ReferenceError, TypeError, cũng như áp dụng các kỹ thuật xử lý lỗi như try...catch, try...catch...finally, throw, Promise.catch() và async/await sẽ giúp lập trình viên dễ dàng kiểm soát và khắc phục lỗi một cách hiệu quả.

Bên cạnh đó, sử dụng các công cụ giám sát lỗi như Sentry, LogRocket để theo dõi và ghi log lỗi sẽ giúp phát hiện và sửa lỗi nhanh hơn trong các dự án thực tế. Quan trọng nhất, cần tuân thủ best practices khi xử lý lỗi, tránh lạm dụng try...catch, đảm bảo thông báo lỗi rõ ràng và chỉ bắt lỗi khi thực sự cần thiết.

Bài viết liên quan