0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

JavaScriptのError handlingについて

Last updated at Posted at 2022-08-12

初めに

今回はエラーハンドリングの基本概念と応用についてまとめていきたいと思います。

参考文章はこちらです。
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]]のErrorconstructor(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のプロパティへアクセスしpropertyError constructor(message)の引数として入れて、自分のフィールドで新しいプロパティnamepropertyを創り出す。

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で分類されたエラーのメッセージをコンソール、またはほかのエラーと予想外のエラーをキャッチします。

簡単にいうと、
エラーがどの段階で出現 ⇒ エラーの細かい分類 ⇒ エラーメッセージの出力や再スローなど

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?