電腦在執行程式的時候,經常會遇到預期的或是非預期的錯誤,TypeScript當然也不例外。這個章節要來介紹TypeScript的「try-catch」錯誤處理機制。



throw關鍵字

TypeScript的throw關鍵字,可以讓JavaScript程式在任意位置中斷執行,並拋出錯誤訊息。這個錯誤訊息可以是任意值(包括undefined)。

例如:

console.log('Hello');

throw `I am an error!`;

console.log('World');

以上程式,會先輸出Hello文字到主控台,接著當程式執行到第三行時,字串I am an error!就會被當作是錯誤訊息被「拋出」。由於第三行敘述的位置並不是在「try-catch」的try區塊下,因此被拋出的錯誤訊息會直接被輸出到主控台中,且若JavaScript環境是Node.js的話,JavaScript程式會立刻結束執行。

try-catch

利用trycatch關鍵字組成兩個程式敘述區塊,可以將try區塊中拋出的錯誤訊息給「攔截」下來。當有錯誤訊息被攔截時,JavaScript會立刻跳去執行catch區塊內的敘述。catch關鍵字後必須用小括號()來填入自訂的參數名稱,這個參數即從try區塊中被拋出的錯誤訊息。若try區塊並沒有拋出錯誤訊息,catch區塊的程式敘述就不會被執行。

例如:

console.log('Hello');

try {
    throw `I am an error!`;

    console.log(`This can't be executed.`);
} catch (err) {
    console.log(err);
}

console.log('World');

以上程式的輸出結果如下:

Hello
I am an error!
World

雖然TypeScript有蠻強大的型別功能,但對於catch關鍵字後小括號()中定義的參數名稱,我們完全沒有辦法去限制它的型別。換句話說,這個參數的型別永遠會被TypeScript當作any

try-catch-finally

我們可以在catch區塊後再使用finally關鍵字來撰寫當try區塊或是catch區塊執行完之後要執行的程式區塊。

例如:

console.log('Hello');

try {
    throw `I am an error!`;

    console.log(`This can't be executed.`);
} catch (err) {
    console.log(err);
} finally {
    console.log('Done!');
}

console.log('World');

以上程式的輸出結果如下:

Hello
I am an error!
Done!
World

專門用來「被拋出」的物件

雖然TypeScript的throw關鍵字可以拋出任意值,但我們還是應該要儘量使用JavaScript內建的專門用來「被拋出」的物件。

RangeError

RangeError物件可用來表示輸入的數值超出範圍。

例如:

try {
    const n = 1.2345;

    console.log(n.toFixed(1000));
} catch (err) {
    console.log(err instanceof RangeError);
}

以上程式會輸出true,因為數值的toFixed方法的第一個參數只能是0 ~ 20的數值,若為負數或是超過20(或超過20太多),就會拋出RangeError物件。

我們也可以自己「new」一個RangeError物件來拋出,RangeError的建構子可傳入一個字串作為錯誤描述。如下:

function discount(price: number, percentage: number): number {
    if (percentage >= 0 && percentage <= 100) {
        return price * (1 - percentage / 100);
    } else {
        throw new RangeError('The form of the percentage needs to be from 0 to 100.');
    }
}

try {
    console.log(discount(100, 20));
    console.log(discount(100, 999));
} catch (err) {
    console.log(err);
}

以上程式的輸出結果如下:

RangeError: The form of the percentage needs to be from 0 to 100.
    at discount (index.js:7:15)
    at Object.<anonymous> (index.js:12:17)
    at Module._compile (internal/modules/cjs/loader.js:774:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:785:10)
    at Module.load (internal/modules/cjs/loader.js:641:32)
    at Function.Module._load (internal/modules/cjs/loader.js:556:12)
    at Function.Module.runMain (internal/modules/cjs/loader.js:837:10)
    at internal/main/run_main_module.js:17:11

相信您應該發現了,RangeError物件除了有輸出我們傳入其建構子作為參數的The form of the percentage needs to be from 0 to 100.字串外,還有輸出RangeError物件被拋出的位置,以及相關的堆疊解開(stack unwinding)資訊。(只不過這邊輸出的位置是指JavaScript程式的位置,並不是TypeScript程式。)

這就是為什麼前面會提到,throw關鍵字應該要儘量使用JavaScript內建的專門用來「被拋出」的物件啦!用這樣的物件才能偵錯嘛~

ReferenceError

當我們在JavaScript中使用JavaScript不認識的名稱時,就會拋出這個物件。當然,因為我們是用TypeScript來開發JavaScript程式,這個狀況並不會很常遇到。

例如:

declare const n: number;

try {
    console.log(n);
} catch (err) {
    console.log(err instanceof ReferenceError);
}

以上程式會輸出true,因為程式第一行只是在TypeScript中「假宣告」出n這個常數。所以程式第四行要存取n時會拋出ReferenceError物件。

SyntaxError

JavaScript內建的eval函數有一個參數,可以將JavaScript程式碼作為字串傳入,如此一來就能夠在程式執行階段動態地產生並執行JavaScript程式碼。

例如:

const result = eval(`
try {
    const result = eval(`
        function addOne(x) {
            return x + 1;
        }
    
        addOne(1);
    `);

    console.log(result);
} catch (err) {
    console.log(err instanceof SyntaxError);
}

以上程式會輸出2

當傳進eval函數的字串內的JavaScript的語法有錯誤時,就會拋出這個例外。

例如:

try {
    const result = eval(`
    function addOne(x) {
        return x + 1;
    
    
        addOne(1);
    `);

    console.log(result);
} catch (err) {
    console.log(err instanceof SyntaxError);
}

以上程式會輸出true,因為我們把addOne函數的結尾大括號}刪除了。

TypeError

假如f是一個函數,則f()敘述可以呼叫它,但如果f不是一個函數,此時用f()嘗試呼叫它的話,就會拋出TypeError物件;假如T是一個類別或是函數,則new T()敘述可以建立其物件實體,但如果T不是一個類別或是函數,此時用new T()敘述嘗試建立其物件實體的話,就會拋出TypeError物件。當然,因為我們是用TypeScript來開發JavaScript程式,這個狀況並不會很常遇到。

例如:

const T = {
    f1: 1,
    f2: '2'
};

try {
    console.log(new (T as any)());
} catch (err) {
    console.log(err instanceof TypeError);
}

以上程式會輸出true,因為物件T只是大括號物件,無法直接被「new」出物件實體。

URIError

JavaScript內建的encodeURIdecodeURI函數可以用來安全地編碼與解碼URI。

用法如下:

const originalURL = 'https://magiclen.org/中文';

const encodedURL = encodeURI(originalURL);

console.log(encodedURL);

const decodedURL = decodeURI(encodedURL);

console.log(decodedURL);

以上程式的輸出結果如下:

https://magiclen.org/%E4%B8%AD%E6%96%87
https://magiclen.org/中文

如果只是要編碼或解碼URI的其中一個部件(component),可以使用JavaScript內建的encodeURIComponentdecodeURIComponent函數。

用法如下:

const originalURLComponent = '中文';

const encodedURLComponent = encodeURIComponent(originalURLComponent);

console.log(encodedURLComponent);

const decodedURLComponent = decodeURIComponent(encodedURLComponent);

console.log(decodedURLComponent);

以上程式的輸出結果如下:

%E4%B8%AD%E6%96%87
中文

在使用encodeURIdecodeURIencodeURIComponentdecodeURIComponent函數時,若輸入的參數有誤(非正確格式的URI),就會拋出URIError物件。

例如:

try {
    console.log(encodeURI('\uD800'));
} catch (err) {
    console.log(err instanceof URIError);
}

try {
    console.log(decodeURI('%'));
} catch (err) {
    console.log(err instanceof URIError);
}

try {
    console.log(encodeURIComponent('\uD800'));
} catch (err) {
    console.log(err instanceof URIError);
}

try {
    console.log(decodeURIComponent('%'));
} catch (err) {
    console.log(err instanceof URIError);
}

以上程式的輸出結果如下:

true
true
true
true

雖然不是很重要,但還是提一下。JavaScript無法編碼含有\uD800 ~ \uDFFF字元的字串。

\u是JavaScript的字元表示方式,其後可以接上2個位元組長度的16進制數值,作為這個字元的字元值。另外還有\x,可以接上1個位元組長度的16進制數值。

Error

上面介紹的RangeErrorReferenceErrorSyntaxErrorTypeErrorURIError都繼承自Error。我們可以把任何不屬於上述錯誤的錯誤都用Error物件來表示。

Error的建構子可傳入一個字串作為錯誤描述。如下:

try {
    throw new Error('Something wrong!');
} catch (err) {
    console.log(err);
}

以上程式會輸出:

Something wrong!

拋出Error相關物件時不使用new關鍵字

RangeErrorReferenceErrorSyntaxErrorTypeErrorURIErrorError除了可以被「new」出物件實體之外,也可以直接用呼叫函數的方式來讓它們回傳自己的物件實體。

例如:

try {
    throw new Error('Something wrong!');
} catch (err) {
    console.log(err);
}

可以改寫成:

try {
    throw Error('Something wrong!');
} catch (err) {
    console.log(err);
}

這樣就可以少打幾個字啦!

總結

在這個章節中,我們學會了TypeScript程式的錯誤處理方式。下一章要來介紹ES6之後加入的神器──產生器(Generator)。

下一章:迭代器(Iterator)與產生器(Generator)