SpringBoot × axios の例外処理の実装方法をまとめます。
通常SpringBootで例外が発生した際、SpringBootの共通で設定したエラー画面に遷移する仕様なのですが、axiosの場合は、javascriptでレスポンスを受け取るので、エラー内容を表示するには個別の制御が必要になります。
Controllerの戻り値について以下の2パターンで実装例を紹介します。
①Map<String, Object>
②ResponseEntity<Map<String, Object>>
Controller設計方針① 戻り値 Map<String, Object>
-
try-catchを記載する
業務処理中の例外をキャッチして、整形済みのJSONレスポンスを返すために使用します。 -
throws の記載は不要
throws
は呼び出し元に例外を伝播するためのものですが、Controllerは最上位のエンドポイントです。
ここでthrows
を付けると、Springが例外をキャッチできず、意図しないレスポンス(HTMLエラー画面など)が返る可能性があります。 -
入力チェック(バリデーション)は try-catch とは別ブロックで記載する
バリデーションは業務例外とは異なるため、bindingResult.hasErrors()
で分岐し、早期リターンするのが望ましいです。 -
戻り値を
Map<String, Object>
にする
フロント(JavaScript)側では、すべてのレスポンスを HTTP 200 として受け取ることになります。
success
やbindErrors
のキーを見て、処理を分岐させる設計になります。
サンプルコード 戻り値 Map<String, Object>
@PostMapping(regist)
@ResponseBody
public Map<String, Object> regist(@ModelAttribute @Valid TestForm form, BindingResult bindingResult) {
// 入力チェック
if (bindingResult.hasErrors()) {
Map<String, Object> bindErrors = bindingResult.getModel();
return Map.of("success", false, "bindErrors", bindErrors);
}
try {
Dto dto = toDto(form); // 変換処理メソッドは割愛します。
registService.regist(form); // 登録処理メソッドは割愛します。
return Map.of("success", true);
} catch (Exception e) {
return Map.of(
"success", false,
"message", "予期せぬエラーが発生しました",
"errorCode", "E999"
);
}
💡 補足:レスポンスの構造
- 成功:
HTTPステータス:200
、success: true
- 入力チェックエラー:
HTTPステータス:200
、success: false
、bindErrors
- その他例外:
HTTPステータス:200
、success: false
、message
、errorCode
javascript設計方針① 戻り値 Map<String, Object>
-
thenブロック内に記載する
サーバーからの戻り値がMap<String, Object>
だった場合、すべてHTTPステータス200として受け取ることになります。よってHTTPステータスで制御できないので、successの値やbindErrorsの有無で条件分岐させます。
javascript
axios.post('/regist', formData)
.then(response => {
const data = response.data;
if (data.success) {
// 正常処理
alert('登録が完了しました');
} else if (data.bindErrors) {
// 入力チェックエラー
console.log('バリデーションエラー:', data.bindErrors);
showValidationErrors(data.bindErrors); // エラー表示メソッドは割愛します。
} else {
// その他の例外
console.error('サーバー例外:', data.errorCode, data.message);
alert(data.message || '予期せぬエラーが発生しました');
}
})
.catch(error => {
console.error('通信エラー:', error);
alert('通信に失敗しました。時間をおいて再度お試しください。');
});
Controller設計方針② 戻り値 ResponseEntity<Map<String, Object>>
-
戻り値を
ResponseEntity<Map<String, Object>>
にする場合
サーバー側での例外状況に応じて、HTTPステータスコードを柔軟に変更できます。
JavaScript側ではresponse.status
を使って、ステータスコードによる制御も可能になります。
ResponseEntityのメソッドによって、HTTPステータスを変更することが可能です。
ResponseEntity の主なメソッド一覧
メソッド | HTTPステータス | 用途 |
---|---|---|
ResponseEntity.ok(body) |
200 OK |
正常レスポンス |
ResponseEntity.badRequest().body(body) |
400 Bad Request |
バリデーションエラーなど |
ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(body) |
500 Internal Server Error |
サーバーエラー |
サンプルコード 戻り値 ResponseEntity<Map<String, Object>>
@PostMapping(regist)
@ResponseBody
public ResponseEntity<Map<String, Object>> regist(@ModelAttribute @Valid TestForm form, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
Map<String, Object> bindErrors = bindingResult.getModel();
return ResponseEntity.ok(Map.of("success", false, "bindErrors", bindErrors));
}
try {
Dto dto = toDto(form); // 変換処理メソッドは割愛します。
registService.regist(dto); // 登録処理メソッドは割愛します。
return ResponseEntity.ok(Map.of("success", true));
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(
Map.of(
"success", false,
"message", "予期せぬエラーが発生しました",
"errorCode", "E999"
)
);
}
}
💡 補足:レスポンスの構造(HTTPステータスを活用)
- 正常 :
HTTPステータス:200 OK
、{"success": true}
- 入力チェックエラー:
HTTPステータス:200 OK
、{"success": false, "bindErrors": {...}}
- その他の例外:
HTTPステータス:500 Internal Server Error
、{"success": false, "message": "予期せぬエラーが発生しました", "errorCode": "E999"}
フロント側ではHTTPステータスによって、ブロックを分けて記述する必要があります。200~300台がthenブロックでそれ以外がcatchブロックです。
axiosの挙動とHTTPステータスの関係
HTTPステータス | 説明 | axiosの挙動 | 入るブロック |
---|---|---|---|
200〜299 | 正常レスポンス | 成功とみなす | .then() |
300〜399 | リダイレクト | 成功とみなす(※) | .then() |
400〜499 | クライアントエラー(Bad Requestなど) | エラーとみなす | .catch() |
500〜599 | サーバーエラー(Internal Server Errorなど) | エラーとみなす | .catch() |
ネットワークエラー | 通信失敗・タイムアウトなど | エラーとみなす | .catch() |
※ リダイレクトはブラウザや設定によって挙動が異なる場合があります。
javascript
axios.post('/regist', formData)
.then(response => {
const data = response.data;
if (data.success) {
alert('登録が完了しました');
} else if (data.bindErrors) {
showValidationErrors(data.bindErrors);
} else {
alert(data.message || '予期せぬエラーが発生しました');
}
})
.catch(error => {
// ここで500などのHTTPエラーを処理
if (error.response && error.response.status === 500) {
const data = error.response.data;
console.error('サーバー例外:', data.errorCode, data.message);
alert(data.message || 'サーバーでエラーが発生しました(500)');
} else {
console.error('通信エラー:', error);
alert('通信に失敗しました。時間をおいて再度お試しください。');
}
});
Service設計方針
Service層では、意図的に例外処理を行いたいケースを除き、try-catch
や throws
といった例外処理を明示的に記述する必要はありません。
なぜなら、Service層で発生した例外は、呼び出し元である Controller 層で制御する設計とするためです。
(※ただし、Controller側で例外を網羅的に try-catch
していることが前提です)
また、IOException
のようにチェック例外(checked exception)を throws
で明示しなければならないメソッドがある場合でも、
EclipseなどのIDEを使用していれば、記載漏れがあるとコンパイルエラーとして警告してくれます。
そのため、警告が表示された箇所にのみ throws
を付与すれば十分です。
このように、Service層では例外処理の責務を持たせず、Controller層で一元的に例外をハンドリングすることで、 コードの見通しや保守性を高めることができます。
Service層サンプルコード
// Service層では、try-catchもthrowsも基本的に実装不要。
public void insert(Dto dto) {
registMapper.insert(dto);
}