初めに
今回はエラーハンドリングの基本概念と応用についてまとめていきたいと思います。
参考文章はこちらです。
Error handling, "try...catch"
Custom errors, extending Error
try...catch(...finally)
動作の仕方
- 実行は
try{}
から始まり、エラーがなければcatch(err)
が無視されます。 - エラーがあれば即時に停止し、
catch(err)
に入ります。 -
finally{}
ではエラーあるなしに関わりなく必ず実行する内容です。
注意点
- 実行段階(runtime)に有効なコードだけ動作します。
- 解析段階(parse-time)でエラーが出たらコード自体が実行不能になるからです。
- Compiler Theory
-
setTimeout()
のような非同期Web APIsが別のところで処理するので、外側にあるcatch(err)
は関数の内側にあるエラーをキャッチできません。
throw
error message & rethrowing
// let json = '{"age": 30}'
let json = '{"age": 30, "name": ""}'
try {
// a
let user = JSON.parse(json)
if (!user.name) {
throw new SyntaxError('Incomplete data: no name')
}
console.log(user.name)
} catch (err) {
if (err instanceof SyntaxError && err.message.indexOf('no name')) {
console.log(`JSON Error: ${err.message}`)
} else {
console.log(`${err.name}: ${err.message}`)
}
}
// ReferenceError: a is not defined
// JSON Error: Incomplete data: no name
instanceof
でインスタンスの検定や、メッセージのキーワードから特定してエラーの分類をやってみました。
↓は再スローのところで、外側からもう一度try...catch
でエラーをキャッチしようとします。
function readJsonData() {
let json = '{"age": 30, "name": "Mick"}'
try {
let user = JSON.parse(json)
if (!user.name) {
throw new SyntaxError('Incomplete data: no name')
}
// if user.name error occurred, it will jump to catch, and never execute b()
b()
} catch (err) {
if (err.name === 'SyntaxError' && err.message.indexOf('no name')) {
console.log(`JSON Error: ${err.message}`)
} else {
throw err
}
}
}
// it is a double-check
try {
readJsonData()
} catch (err) {
console.log(`External catch: ${err}`)
}
// External catch: ReferenceError: b is not defined
でも実際やってみたら、このやり方はダブルチェックみたいな行いだと感じています。なぜならtry{}
で複数のエラーが出ても最初だけ反応してすぐcatch(err){}
に入ります。
再スローはtry{}
もcatch(err){}
でのエラー分類も完璧だと思って、予期せぬエラー(分類のルートに入っていない)がでたら外側で対応できる方法だと思います。
...finally{}
実行順:
エラーがない場合は、try
⇒ finally
エラーがある場合は、try
(中断) ⇒ catch
⇒ finally
// ues devTool
// prompt(message, default)
let num = +prompt('Enter a positive integer number?', 35)
let diff, result
function fib(n) {
// if n is negative or not an integer
if (n < 0 || Math.trunc(n) !== n) {
throw new Error('Must not be negative, or an integer')
}
// if n <= 1 is true, return itself, or fib() recursion
return n <= 1 ? n : fib(n - 1) + fib(n - 2)
}
let start = Date.now()
try {
result = fib(num)
} catch (e) {
result = 0
} finally {
diff = Date.now() - start
}
console.log(result || 'error occurred')
console.log(`execute took ${diff / 1000}sec`)
// 35.1
// error occurred
// execute took 0sec
// 35
// 9227465
// execute took 0.138sec
finally{}
は必ず実行するコードなので、try...catch
構文で計測や最終の確認などよく使われているようです。実行する内容がなければ省略していいです。
finally vs. return
try...catch...finally
構文ではfinally{}
はtry{}
よりも後ろにいると見えるが必ず実行するコードなので、またreturn
は外側に戻る最後のステップとしてfinally{}
よりも先に実行しません。(try{}
でreturn
よりも先にエラーが発見する場合はreturn
は実行されずcatch(err){}
に入り、そしてfinally{}
へ。)
function func() {
try {
return 1;
} catch (err) {
/* ... */
} finally {
console.log('finally');
}
}
console.log(func())
// finally
// 1
Custom errors, extending Error
Extending Error
ここからは組み込みメソッドError
へ拡張し、Error
内部のプロパティを利用した派生クラスのインスタンスの使い方を紹介したいと思います。
let json = `{"name": "Mick", "age": 30}`
class ValidationError extends Error {
constructor(message) {
// [[HomeObject]] => Error constructor(message)
super(message)
this.name = 'ValidationError'
}
}
function test() {
throw new ValidationError('Error Occurred!')
}
try {
test()
} catch (err) {
console.log(err.message)
console.log(err.name)
}
// Error Occurred!
// ValidationError
super(message)
で[[HomeObject]]のError
のconstructor(message)
へアクセスし、親であるError
のプロパティを利用しながら、自分のクラスにthis.name
へ値を指定する。
それでインスタンスのnew ValidationError
はカスタムエラーを創り出します。
↓は使用例ですが。
// Usage
function readUser(json) {
let user = JSON.parse(json)
if (!user.age) {
throw new ValidationError('No field: age')
}
if (!user.name) {
throw new ValidationError('No field: name')
}
return user
}
try {
let user = readUser('{"age": 25}')
} catch (err) {
if (err instanceof ValidationError) {
console.log(`Invalid data: ${err.message}`)
} else if (err instanceof SyntaxError) {
console.log(`JSON Syntax Error: ${err.message}`)
} else {
throw err
}
}
// Invalid data: No field: name
要するにerr
オブジェクトのinstanceof
検定で親であることでエラーの分類を行います。
Further extending
さらなる派生クラスへ拡張したらどうなるでしょう。
// further extending
class PropertyRequiredError extends ValidationError {
constructor(property) {
// [[HomeObject]] => ValidationError constructor(message) => Error constructor(message)
super(`No property: ${property}`)
this.name = 'PropertyRequiredError'
this.property = property
}
}
// Usage
function readUser(json) {
let user = JSON.parse(json)
if (!user.age) {
throw new PropertyRequiredError('age')
}
if (!user.name) {
throw new PropertyRequiredError('name')
}
}
class PropertyRequiredError
は先ほどと同じくsuper()
で親のError
のプロパティへアクセスしproperty
をError constructor(message)
の引数として入れて、自分のフィールドで新しいプロパティname
とproperty
を創り出す。
instanceof
演算子はインスタンスの管理にも使われており、下のように各エラーのルートに入ります。
console.log(new PropertyRequiredError instanceof Error) // true
console.log(new PropertyRequiredError instanceof ValidationError) // true
console.log(new PropertyRequiredError instanceof PropertyRequiredError) // true
try {
let user = readUser('{"age": 25}')
} catch (err) {
if (err instanceof ValidationError) {
console.log(`Invalid data: ${err.name}, ${err.message}`)
} else if (err instanceof SyntaxError) {
console.log(`JSON Syntax Error: ${err.message}`)
} else {
throw err
}
}
// Invalid data: PropertyRequiredError, No property: name
Wrapping exceptions
いくら万全だと思ってもアクシデントが発生するかもしれません。しかしエラーのチェックいちいちやると時間や手間がかかってしまう。したがってエラータイプの分類や管理のしかたが重要になります。
まずは概ねどんな段階でエラーが発生したのか把握するならReadError
読み込みエラーとか設定して、
// if error occurred at the phase of reading data, create the corresponding error message
class ReadError extends Error {
// [[HomeObject]] => Error constructor(message)
constructor(message, cause) {
super(message)
this.cause = cause
this.name = 'ReadError'
}
}
その下にはまた細かい分類があれば、前の例のようにバリデータクラスを作り、有効データであるか、そしてさらに派生クラスにデータのプロパティなど細部のチェックをします。
class ValidationError extends Error {
constructor(message) {
// [[HomeObject]] => Error constructor(message)
super(message)
this.name = 'ValidationError'
}
}
class PropertyRequiredError extends ValidationError {
constructor(property) {
// [[HomeObject]] => ValidationError constructor(message) => Error constructor(message)
super(`No property: ${property}`)
this.name = 'PropertyRequiredError'
this.property = property
}
}
validateUser
関数で細かいチェックを実行させたり、readUser
で全体的なチェック(データ構文上のエラーか、データ内部のプロパティが見つからないか)try...catch
を入れます。
// check json data and console the invalid property
function validateUser(user) {
if (!user.age) {
throw new PropertyRequiredError('age')
}
if (!user.name) {
throw new PropertyRequiredError('name')
}
}
// check json data by try...catch
function readUser(json) {
let user
// check1: the err is SyntaxError instance or not
//
try {
user = JSON.parse(json)
} catch (err) {
if (err instanceof SyntaxError) {
throw new ReadError('Syntax Error', err)
} else {
throw err
}
}
// check2: the err is ValidationError instance or not
try {
validateUser(user)
} catch (err) {
if (err instanceof ValidationError) {
throw new ReadError('Validation Error', err)
} else {
throw err
}
}
}
try {
readUser('{bad json}')
} catch (err) {
// catch error throw from readUser()
if (err instanceof ReadError) {
console.log(err)
console.log(`Original error: ${err.cause}`)
} else {
throw err
}
}
// ReadError: Syntax Error
// at readUser ....
// Original error: SyntaxError: Unexpected token b in JSON at position 1
最後はもう一回try...catch
で分類されたエラーのメッセージをコンソール、またはほかのエラーと予想外のエラーをキャッチします。
簡単にいうと、
エラーがどの段階で出現 ⇒ エラーの細かい分類 ⇒ エラーメッセージの出力や再スローなど。