Exceptionをもみ消すなってどうせえちゅうねんって話

QiitaのJavaアドベントカレンダー14日目になります。

若干時間オーバーです、ごめんなさい、時差的にこちらではセーフなので許してください。 うそやんけ、僕の担当14日でした。 今日15日・・・圧倒的遅刻・・・・ごめんなさい!!!!

そういえば、最近開発者コミュニティに入りました。 初学者が多めのコミュニティのようですが、それでも勉強になることが多く、参加してよかったなと感じています。

主にSlackで活動しているようです。

海外からでも参加できるため、非常にありがたいです。 僕は現地語がまだ喋れないので、現地の開発者コミュニティにはまだ参加できていないんですよね。。。。

さて、というわけで、今回は初学者向けのエラーハンドリングの記事を書いていきたいと思います。

基本的にはJavaで書かれていますが、他の言語でも参考になることはあるかと思いますので、初学者でもみ消すなに手をコマネいている方はぜひ読んでみてください。

そもそも揉み消しとは

仕事でコードを書き始めた人は、先輩やマネージャから「Exceptionをもみ消すな」という言葉を聞いたことがあるかと思います。
具体的に言うと、こんな感じのコード。

static void momikeshiTheException(SomeObject input) {  
  try {  
    someIoOperation(input);  
  } catch (IOException e) {  
    e.printStackTrace();  
  }  
}  

ひどい場合だとこう。

static void momikeshiTheException(SomeObject input) {  
  try {  
    someIoOperation(input);  
  } catch (IOException e) {}  
}  

初学者のうちはExceptionの扱い方がわからずこういったコードを書いてしまいがちです。
なんなら、僕も学生の頃はよく書いてました。

このコードには以下のような問題があります。

  • エラーが発生した事実が呼び出し元に通知されないため、呼び出し元は成功したものとして処理を続けてしまう。
  • エラーの詳細が記載されていないため、デバッグが困難になる

初学者や自習で学んでる人たちには、現場でどんな不都合が起きるかイメージしづらい部分があるかもしれないので、具体的なケースを挙げてみる。

だらだら長いので、読みたくない方は飛ばしてください笑

簡単に説明すると、レストランの発注システムでExceptionがもみ消されたせいで、季節限定メニューのオーダーが厨房に届かずキャンペーンが失敗して、そのデバッグに受注したフリーランスの私が苦しむ話です。

コスタリカブルーの悲劇

喫茶店コスタリカブルーでは、2015年の開店以来、ずっと手書きの伝票を使っていた。

しかし、スマホ決済が徐々にメジャとなってきていること、また来年には新たに支店を開設することを鑑み、2019年の夏から注文・決済・記帳フローの自動化に踏み切った。

友人のつてで紹介されたフリーランサーによって作成したアプリでは、スマホで注文を受けを受けることができ、注文内容はキッチンとレジスターに送られる。

会計は現金もしくは任意のスマホ決済で行え、それぞれの会計内容は帳簿に自動で記帳される。

夏に導入してから半年、初期のバイトの子たちへの講習に手間取った以外に大きな問題はなく、システムは概ね好調に稼働しているように思われた。

しかし、10月に入り事態は一変した。

コスタリカブルーでは、10月に入り、ハロウィンにあやかり、ハロウィンランチセットの提供を始めた。

ハロウィンランチを注文した顧客は、パスタ・サンドウィッチ・ロコモコのうちからメインを1品を選ぶことができる。

事前の試食会での反応は上々で、料理長をはじめとするスタッフ一同は、それなりの手応えを感じていた。

しかし、彼らの期待に反し、ハロウィンランチセットの提供初日、店内は大混乱した。

ハロウィンランチセットのオーダーがキッチンに届かないのである。

正確には、パスタセットは届くのだが、サンドウィッチとロコモコが届かない。

厨房スタッフは何も知らないまま来たものを順番に作り、フロアのスタッフは何か遅いなと思いつつも提供を続けた。

20分後。あるテーブルからクレームが入る。

四人のグループ席で注文したハロウィンセットのうち、ロコモコとサンドウィッチを頼んだ三人の分がまだ来ていないと言う。

残ったパスタを頼んだ一人も、食べずに残りの三人の分を待っていたようで、すでにパスタの表面は乾き始めている。

フロアスタッフは厨房に駆け込み、いつ出来上がるかと厨房スタッフに尋ねるが、厨房スタッフはそんなオーダーは入っていないという。

何かがおかしい。

フロアスタッフはシェフに伝票を見せ、急ぎでロコモコとサンドウィッチを作り始めてもらうよう頼み、お客様の待つテーブルに戻る。

しかし、四人組は今から作るならもういらないと、怒りながら店を後にしてしまった。

それもそのはずである。
コスタリカブルーのランチタイムのメインターゲットはオフィスレディたちであり、少ない昼休憩を使ってコスタリカブルーでランチを取ってくれている。

時間になればオフィスに帰り仕事に戻らなくてはいけない彼女たちに取って、今から作り直すなど、10分とて待つのは難しいのである。

また、このやりとりを見ていた周りの客たちも口々にハロウィンセットのオーダーをキャンセルし、店を後にした。

システムが故障しているらしいと察したフロアのスタッフは、持っていたペンとメモ帳でオーダーを続けたが、すでにランチタイムは終了間近。
結局初日のランチセットは売り上げがほぼないまま終了してしまった。

その夜、連絡を受けたオーナーはフリーランサーに連絡し、翌日までにバグの修正をするよう依頼した。

フリーランサーは深夜の連絡に気を滅入らせながらlogを確認した。
エラーログにはいくつかスタックトレースが出力されていたが、どれも時間が記載されておらず、また問題が起きたクラスの行と名前以外何もわからない。

会計未終了通知サービスクラス。これは違うだろう。
帳簿の勘定項目のDaoクラス。これは毎月月末にバックグラウンドで深夜に走るバッチで使うクラスだ、違うだろう。

……(2時間後)

オーダーの備考マスターのDaoクラス。
もしかしてこれか?

オーナーに連絡し、DBの閲覧許可をもらい、オーダーの備考マスタを確認する。

無い。

ハロウィンランチセットの厨房への注文票に追加で出力される備考マスターに何もデータがないのだ。

備考マスタはハロウィンセットのような1つのメニューに複数の選択肢がある場合に使われるマスターだ。

幸か不幸か、これまでコスタリカブルーはそのようなセットを設けてこなかった。
したがって、このテーブルを利用される機会がなく、半年間このバグに遭遇することがなかったのだ。

このマスタが存在しないと言うことは、備考登録時に何らかのバグが発生し、登録が完了しなかったに違いない。

さらに過去のログを見る。
時刻はすでに二時を回っている。

……2時間後
見つけた。備考マスタを登録するときに、ClassCastExceptionが吐かれている。

しかし、何がcastされているんだ…

例外はわかった。しかし、Inputがわからない以上、ソースを追うしかない。
時刻は午前4時。明日の仕事には確実に響くだろう。

結局、1時間にわたるデバッグの結果、テーブルに保存するEntityクラスのプロパティのenumが、きちんとvalueに変換せずそのままDBにinsertされていたため、enumのordinalで保存しようとしていたのが原因だった。

修正は非常に簡単なため、30分で修正を終え、テスト後デプロイし直した。

時刻はすでに6時。彼は睡眠を諦め、レッドブルを買いにコンビニへ向かうことにした。

ではどのように例外を取り扱えばいいか

さて、上記のような悲劇を避けるために我々はどのようにExceptionを扱えばいいのでしょうか。

そもそも論でいうと、実はこのエラーハンドリングが商用プログラムを書く上で最も重要になってくる箇所であり、このエラーハンドリングをいかに上手くやるかが腕の見せ所だったりします。

この記事では、僕が経験的に例外はこう言う風に扱うといいよと言う例をいくつか提示していこうと思います。

鉄則と言うか絶対やらなきゃいけないこと

例外が発生した場合、開発者が絶対にやらなくてはいけないことがあります。

具体的にいうと次の二つ。

  • エラーの内容を、いつ、どんな状況で、何の入力に対して例外が発生したかをlogに出力する
  • エラーが発生したことを呼び出し元のクラスやユーザーに対して通知する。

この2つのうち、2番目に関してはこの後、順番に例示していきますが、1つ目に関しては非常に明快です。

すぐできます。

ほとんどの言語にはloggerを提供するライブラリが存在するため、そういったライブラリを用いてlogを出力していきます。

Loggerライブラリは様々ありますが、基本的にはプロジェクトの標準で使っているloggerライブラリを使えば問題ないかと思います。

オレオレで自分の好きなライブラリを勝手に使い出すと別の意味で怒られます。

他の人のコードなどを参考に何が使われているか調べましょう。

System.out.printlnやconsole.logが標準ならそのプロエジェクとはもうダメだ。転職を考えよう。

logに何を出力するか

さて、冗談はさておき。。

そういったライブラリは、基本的に発生時間と発生クラスをプリントし、また発生した例外を引数に加えることでスタックトレースを出力してくれます。
したがって、あとはメッセージにinput内容と、何をしようとした時に起きたかを書けば最低限何かが起こっても対応出来ます。

例えばこんな感じ。

static void momikesanaiTheException(SomeObject input) {  
  try {  
    someIoOperation(input);  
  } catch (IOException e) {  
    log.error("Exception occurred while calling the someIoOperation with the input {}", input, e);  
  }  
}  

inputに関しては必要に応じてマスクしたり、プロパティを絞ったりする必要があるが、何の入力に対してエラーが発生したかを記述しておくことは、圧倒的にdebugコストを下げます。
したがって、パスワードや個人情報などには留意した上で、inputも出来るだけ詳細に出力するように心がけるべきです。

もっと言うなら、影響範囲や起こりうる障害、対応方法なども書いておくと、将来別の開発者の手にメンテが渡った時に役立ちます。
これに関してQiitaで自作のライブラリを作ったって言う超良記事があったのだけど、いつの間にかストックから消えていたので、知ってる人がいたらコメント欄に書いてもらえるととっても嬉しい、です。

Loggingに関していえば、それだけで僕でも2、3記事書けるくらい実は奥深いコンテンツなので、今回はいったんこの程度でまとめさせていただきます。

logの出力に関する注意

初学者向けの記事につき、一応注意喚起しておきます。
商用含め、自分以外に公開されるのプログラムでは、ユーザの個人情報やパスワード、クレジットカード情報など、logに出力してはいけない情報が山ほどあります。

ちなみに、Facebookですらやらかしてます。
参考: Facebookが数億人のパスワードを平文で保存していたと認める

こういった情報は必ずマスクするか、出力から外すようにしましょう。

Javaでは、Objectをloggerや標準出力に渡すと、instanceをtoStringした値を出力します。

したがって、toStringを適切に実装してやることが非常に重要です。
lombokであれば@ToString.Excludeをプロパティにつけてやることで、toStringの対象から外すことができます。

気づけば当たり前のことなので、logを出力する際には、注意して実装してください。

また、出力していいかわからないときは、上司やクライアントに必ず確認をとるようにしてください。

あ、パスワードとかクレジットカード情報とかは、上司やクライアントがいいっていってもダメです。

呼び出し元に通知する方法

さて、ここからは、呼び出し元のクラスやユーザに対して失敗した事実を通知する方法に関して書いていきます。

僕が一番使っている期間が長いことや、Javaアドベントカレンダーの記事であることを鑑みて、今回はJavaでかかせていただいております。

他の言語の方には申し訳ありません。。(´・ω・`)

後半にいくにつれて実装難易度(って言っても大したことないけど)が上がっていくように書こうと思っています。現時点では。

また、それぞれの実装方法に僕の独断と偏見で以下のようなランクづけをしました。

この基準が役に立つかは知らないし、状況や実装ロジックによって安全性や実用性は変わるので何とも言えません。

ただ、なんとなく実装の安全さや実用性に関してイメージを掴んでもらえたらいいなと言う意味でつけています。

  • 実装難易度:
    実装の難易度や工数など。高いほど言語に対する理解が必要になり、書かなくてはいけない行数も増える。
  • 実用性:
    実際の現場でどの程度使われるか。難しい実装でも、そこまでする必要はないとか、逆に簡単な実装でもそれでは不十分と判断されることも多いため。
  • 安全性:
    呼び出し元に、プログラム的な意味でどれだけ正確な情報を伝えられるか。正確な情報を呼び出し元に伝えることで呼び出し元はより柔軟に呼び出しの失敗に対応できる。

1: booleanで結果を返す

実装難易度: ★
実用性: ★★
安全性: ★

説明

1つ目は、成功した場合trueを、失敗した場合falseを返すと言う実装です。
非常に簡単で、今この瞬間からもできるので、複雑な実装をしている時間がないと言う場合は、最低限このくらいはするように実装して欲しい。

static boolean momikesanaiTheException(SomeObject input) {  
  try {  
    someIoOperation(input);  
    return true;  
  } catch (IOException e) {  
    log.error("Exception occurred while calling the someIoOperation with the input {}", input, e);  
    return false;  
  }  
}  

このような実装にすることで、最低限、失敗した場合falseが帰ってくることで、呼び出し元のクラスは失敗した場合の処理を流すことができます。

この実装は呼び出し元に失敗の原因が通達されていないため、若干心もとないですが、失敗する条件が明確な時はこれでも十分かと思います。
例えば、JavaのSetのaddなどは、すでに同じ要素がCollectionに存在する時falseを返しますね。

いつ使うべきか

基本的にはあまりおすすめはしませんが、以下のようなケースでは使えるかと思います。

  • 他の開発者もしくはかつての自分が実装したもみ消しを発見してしまい、急ぎ挙動を修正する必要がある場合
  • シンプルかつアトミックなことが自明な場合。

2: Optionalで包んで結果を返す。

実装難易度: ★★
実用性: ★★
安全性: ★

解説

これもほぼbooleanと同じですが、booleanより使える箇所がやや限られます。
Optionalは、詳しくは他の記事を漁って欲しいのだけど、ある操作に対して要素が存在するかしないか不定の時に使われます。
したがって、Optionalで返していいのは、例えば指定したIDやKeyに対して要素が存在しない時に例外が投げられるケースが基本です。
例えば、FileNotFoundExceptionやNoSuchElementExceptionなどはoptionalで包んで返してもいいと思います。

static Optional<FileReader> momikesanaiTheException(SomeObject input) {  
  try {  
    return Optional.of(new FileReader(new File(input.getTargetPath())));  
  } catch (FileNotFoundException e) {  
    log.error("File not found to the path {}", input.getTargetPath(), e);  
    return Optional.empty();  
  }  
}  

実装の手間がbooleanとそう変わらないのに実装難易度を高めにつけたのは、 このOptionalを使っていいかと言う判断が多少経験を要するからです。

いつ使うべきか

正直、上の解説を書いててエラーハンドリングでOptional使うのは微妙かなと思い始めたのですが、以下のSOの記事で条件付きでですが、そこそこ支持されていたので、一応書いてみます。
参考: Can I use std::optional for error handling?

  • IOを伴う操作で指定したリソースが存在しない、もしくは取得できない可能性がある場合
  • 例外を投げたくない場合

なお、後述のEither型の方がより正確に呼び出し元に何が起きたかを伝えられるため、個人的にはEither型をオススメしています。

3: 別の例外を実装し、投げられた例外を包んで呼び出し元に投げる

実装難易度: ★★★
実用性: ★★
安全性: ★★★

解説

ロジックの中で呼び出したメソッドが非チェック例外(※1)を投げる場合、チェック例外(※2)などに包んで返すのも1つの手段です。
呼び出し元にExceptionのハンドリングを委託し、自分のロジック内でのハンドリングを諦めるパターンです。

この場合、呼び出し元のloggerにメッセージが出力されるため、鉄則と言うか絶対やらなきゃいけないことで書いたlogは省略することができる場合もあります。

static void momikesanaiTheException(SomeObject input) throws InvalidInputException{  
  try {  
    someOperation(input);  
  } catch (InvalidSyntaxException e) {  
    throw new InvalidInputException(input, "Input contains some invalid value and some Operation failed.", e);  
  }  
}  
public class InvalidException extends Exception{  
  private final SomeObject input;  
  public InvalidInputException(SomeObject input, String message, Throwable e) {  
    super(message, e);  
    this.input = input;  
  }  
}  
  

チェック例外などと書いたのは、非チェック例外を投げるケースも少なからずあるためです。

非チェック例外を投げる場合は、必ずJavadocのthrows欄に明記するようにしましょう。

※1:RuntimeExceptionもしくはそれを継承した、呼び出し元でtry-catchしなくてもコンパイルエラーが起きない例外。呼び出し元に瑕疵がある場合に投げられることが多い。NullPointerExceptionやIllegalArgumentExceptionなど。

※2:Exceptionもしくはそれを継承した、呼び出し元でのtry-catchを義務付ける例外。I/Oでのエラーやプログラムの実行ユーザーが権限を持っていない場合など、ロジックやinput由来ではない時に投げられることが多い。IOExceptionやExecutionExceptionなど。

いつ使うべきか

実際の開発現場ではこのように独自の例外を実装して呼び出し元に返す方法はよく使われています。
あえて使用場面をあげるなら以下のようになります。

  • 発生した例外に対し、自分の実装したメソッド内での消化が難しい場合
  • 呼び出し先クラスがチェック例外を投げるべきところで非チェック例外を投げている時
  • 例外のハンドリングに自信がない場合(早く抜けてください。許されるのは最初だけです。)

余談

僕は過去ベトナムの開発会社にいた時、JavaDocが全く書かれていないにも関わらず、何でもかんでも例外を非チェック例外に包んで投げまくるのがデフォルトと言う、地獄のようなプロジェクトに参加したことがあります。

その時は、ありとあらゆる箇所でハンドルされない例外が投げられまくり、何かあるとすぐシステムが落ちると言うえげつない状況に陥りました。

4: 返り値のクラスで結果を表現する

実装難易度: ★★★★
実用性: ★★★★
安全性: ★★★

解説

処理の結果をクラスで表現するのもありです。
具体的に書くと、成功した場合の返り値となる値と失敗した場合の情報を1つのクラスにして返す方法です。

例えば、以下のようなResultクラスを実装し、それを呼び出し元に返すようにします。
(@Builderや@Getterと言うのはlombokアノテーションです。詳しくはlombok公式ページへ)

@Builder(access = AccessLevel.PRIVATE)
@Getter
public class MomikesanaiResult {
    private final ResultType result;
  private final List<SomeResult> successResults;
  private final Map<FailedCauseType, List<SomeInput>> failedInputs;

  private MomikesanaiResult(ResultType result, List<SomeResult> successList, Map<FailedCauseType, List<SomeInput>> failedInputs) {
    this.result = result;
    this.successResults = Optional.ofNullable(successList).map(Collections::unmodifiableList).orElse(Collections.emptyList());
    this.failedInputs = Optional.ofNullable(failedInputs).map(Collections::unmodifiableMap).orElse(Collections.emptyMap());
  }

  public static class MomikesanaiResultBuilder {
    public MomikesanaiResultBuilder addSuccess(SomeResult result) {
      if (this.successResults == null) {
        this.successResults = new ArrayList<>();
      }
      this.successResults.add(result);
      this.result = this.failedInputs == null ? ResultType.SUCCESS : ResultType.PARTIALLY;
      return this;
    }

    public MomikesanaiResultBuilder addFailed(FailedCauseType failedCause, SomeInput input) {
      if (this.failedInputs == null) {
        this.failedInputs = new HashMap<>();
      }
      if (!this.failedInputs.containsKey(failedCause)) {
        this.failedInputs.put(failedCause, new ArrayList<>());
      }
      this.failedInputs.get(failedCause).add(input);
      this.result = this.successResults == null ? ResultType.FAILED : ResultType.PARTIALLY;
      return this;
    }
  }
    
  public enum ResultType {
    SUCCESS, FAILED, PARTIALLY
  }

  public enum FailedCauseType {
    NONE,AUTH_INVALID, INVALID_INPUT, TIMEOUT, UNKNOWN
  }
}

このようなクラスを実装することで、以下のように具体的にエラーと結果を受け手に返すことができます。

static void momikesanaiTheException(List<SomeInput> inputs) throws InvalidInputException{  
  val resultBuilder = MomikesanaiResult.builder();  
  for (SomeInput input : inputs) {  
    try {  
      resultBuilder.addSuccess(someOperation(input));  
    } catch (IllegalArgumentException e) {  
      log.error("Input contains some invalid value and some Operation failed. input is {}", input, e);  
      resultBuilder.addFailed(FailedCauseType.INVALID_INPUT, input);  
    } catch (AuthFailedException e) {  
      log.error("Authentication is failed in some Operation. input is {}", input, e);   
      resultBuilder.addFailed(FailedCauseType.AUTH_INVALID, input);  
    } catch (TimeoutException e) {  
      log.error("Request seems to be timeout in some operation, input is {}", input, e);  
      resultBuilder.addFailed(FailedCauseType.TIMEOUT, input);  
    } catch (Exception e) {  
      log.error("Unknown error occured in some operation, input is {}", input, e);  
      resultBuilder.addFailed(FailedCauseType.UNKNOWN, input);  
    }  
  }  
  return resultBuilder.build();  
}  

なお、ResultTypeとFailureCauseTypeをenumにする理由は、受け手側がswitchでより簡易にerrorハンドリングを実装できるようにするためです。
また、Map<FailureCause, List<SomeInput>> のようなコレクションやマップのネストは気持ち悪い場合は別の構造体を定義しても大丈夫です。

いつ使うべきか

かなり実装コストが高いですが、それなりにメリットの大きい実装になります。
使用場面をあげると、

  • 他のサービスや外部の開発チームに提供されるライブラリ・SDKなど
  • パースしてWebApiやXHRの返り値に使う場合(ユーザへの通知はAPIの呼び出し元が行う。webならjavascriptでトースターを表示するなど。)
  • 多くの開発者に使われる基盤クラス
  • 例外を投げたくない場合

などなど、個人的には、後述のEitherが使えない現場では個人的にこの実装を勧めています。

5: Either型で返す

実装難易度: ★
実用性: ★★
安全性: ★★★

解説

Either型は関数型でよく使われる型で、成功した結果もしくは失敗した例外を返すと言う型です。
関数型では、呼び出したメソッドを返り値に置換しても同じ挙動ができることを担保すべしと言う考え方があり、例外を投げること自体があまり好まれません。

したがって、メソッドの返り値として、成功した値もしくは発生しうる例外をEither型で指定することで、例外をthrowするのを回避すると言うやり方が取られることが非常に多いです。

Either型では伝統的にright(右, 正しいと言う意味もある)に成功した時の値を、left(左)に例外を入れて呼び出し元に返します。

なお、Either型はJava標準ではサポートされていないため、Functional Javaなどの依存性を追加するか、自前で実装する必要があります。(サンプルコードはFunctionalJavaEitherを使用)

static Either<FileNotFoundexception, FileReader> momikesanaiTheException(SomeObject input) {  
  try {  
    return Either.right(new FileReader(new File(input.getTargetPath())));  
  } catch (FileNotFoundException e) {  
    log.error("File not found to the path {}", input.getTargetPath(), e);  
    return Either.left(e);  
  }  
}  
  

割と僕の周りではこの型を好む人が多いですが、周囲の開発者が関数型に慣れていない場合、呼び出し元の開発者がうまく使えない可能性が高く、日本の開発者環境においてうまく機能する現場はあまりないかもしれません。

下手に導入するとExceptionよりもみ消されるという結果になることも無きにしも非ずです。

ただし、Eitherは非常に強力な型で、シンプルな実装で安全かつ正確に何が発生したかを呼び出し元に伝えることができます。
したがって、一通り使い方を覚えるだけでかなり表現の幅が上がるため、マスターしておいて損はありません。

詳しく知りたい人は以下の記事を参考にしてみてください。
参考: Lazy Error Handling in Java, Part 3: Throwing Away Throws

また、こちらは僕の愛読書になります。
下のiframeはアフィリエイトなので、アフィなんて死んでも踏みたくないという方はこちらから

僕はScalaは申し訳程度にしか書けませんが、関数型の概念を言葉とコードでかなり詳細に説明してあり、仕事では常に携帯しています。 まじオススメです。

いつ使うべきか

前述の通り、Eitherは非常に強力ですが、受け取り側にもそれなりの知識が必要になるので、周囲の開発者の知識量などに合わせて使うべきです。
使用場面としては、

  • Exceptionを投げたくない場合
  • 失敗した場合と成功した場合の返り値を一つの型で表現したい時
  • 関数型を使う風土が社内もしくは最低限チーム内に共有されている時

となります。

まとめ

以上、初学者向けに例外が発生した場合のハンドリングの仕方を書いてみました。

ほんとはfrontendに絡めたり、呼び出し元がどういう風にこのエラーハンドリングに対して対応するかなども、コードとして書きたかったのですが、時間切れでした。

プログラミングを自習したり、自分専用のアプリを作ったりしてるうちは、例外の処理は甘くなりがちです。
しかし、実際にお客さんに使われるシステムやサービスでは、例外処理とロギングこそ、最も時間と経験と知識を費やす箇所になります。

上記で紹介したエラーハンドリングの方法も、どれか一つだけを使うわけではなく、状況に合わせて組み合わせて使うことも多くあります。
そこらへんのことも、そのうち記事に書きたいなとか、ぼんやり考えています。

あ、あと今回書かなかったけど、呼び出し元がエラーハンドリングしやすいように実装してやるのも大事だよっていうのを書き忘れていたので、補足でそれも意識するといいと思います。(雑ですみません。。。)

以上、Exceptionをもみ消すなってどうせえちゅうねんっていう話でした。
この記事が、上司や先輩から例外をもみ消すなと言われて途方に暮れている初学者の方々の助けになれば幸いです。