実装学習記事全11節Build

承認処理の部分失敗を、正常完了として扱わないように直す

この記事で身につくこと想定外の入力、部分失敗、再実行、記録、読みやすさ、責任の分離を改善する理由を説明できる。

記事内目次(24項目)
この記事の管理情報(該当工程・関連要件・成果物の閲覧と保存・実行記録)
記事ID
08-04
該当工程
08-04 エラー処理と保守しやすさを整える(関連:08-03、04-04、05-02、09-01、09-03)
関連要件
REQ-005、REQ-006、REQ-007
到達目標
想定外の入力、部分失敗、再実行、記録、読みやすさ、責任の分離を改善する理由を説明できる
主要な成果物
確認環境と検証状況
本文のコードは examples/08-04-error-handling/ を正本とし、Claude Codeがローカルで実行して結果を確認済み(初版 2026-09-20。stdout・stderr・終了コードの実行記録は最終監査 2026-09-21 に取得。macOS 26.6.2、Node.js v24.18.0)。修正後も保存先は不変条件を満たさない(第3.3節)。第5.4節の指示文は説明用の例。人による実読・内容確認は未実施。
未検証の範囲
実際の業務で起きうる部分失敗(ディスク容量不足、通信断など)の再現、状態変更のロールバックが必要かどうかの判断は、この記事の時点では確認していない。
実行記録

正本の原稿にある管理表(記事ID・先に読むと理解しやすい記事・成果物パス・最終更新日など)は、この下の本文冒頭に「原稿の管理表」として開閉できる形で残しています。

原稿の管理表(13項目:記事ID・カテゴリ・要件ID・先に読むと理解しやすい記事・成果物パス・対象ツール・検証状況・最終更新日など)
管理項目内容
記事ID08-04
主な中カテゴリ08-04 エラー処理と保守しやすさを整える
関連中カテゴリ08-03、04-04、05-02、09-01〜09-03
関連要件IDREQ-005、REQ-006、REQ-007
先に読むと理解しやすい記事08-02(入力・計算・保存をつなぐ)、08-03(画面・処理・外部機能をつなぐ)を読んでいると理解しやすい。未読でも本文で必要な内容は説明する
到達目標想定外の入力、部分失敗、再実行、記録、読みやすさ、責任の分離を改善する理由を説明できる
3層の配置理解する=第3〜4節/作って確かめる=第5〜7節/設計を判断する=第8節
成果物パスtemplates/08-04-error-handling_template.md、templates/08-04-error-handling_example.md、evidence_public/final-audit-2026-09-21/08-04/run_log.md、examples/08-04-error-handling/
出典第11節参照
対象ツール・バージョン教材演習用にNode.js v24.18.0(node:fs、node:pathのみ使用)
検証状況本文のコードは examples/08-04-error-handling/ を正本とし、Claude Codeがローカルで実行して結果を確認済み(初版 2026-09-20。stdout・stderr・終了コードの実行記録は最終監査 2026-09-21 に取得し evidence_public/final-audit-2026-09-21/08-04/run_log.md に記録。失敗ログの時刻以外の出力は同一。第6〜7節)。第5.4節の指示文は説明用の例であり、実際にAIへ送信・観測した記録ではない。人による実読・内容確認は未実施
残課題第11節参照
最終更新日2026-09-20(新規作成。同日、Phase 5B-2B:受入監査の「修正版でも状態だけ変わって履歴が残らず、記事Aの不変条件を満たさない」指摘に対し、第3.3節「この修正で直ること・直らないこと」と第8.3節「不変条件が満たされない理由と、記事Bへ引き継ぐもの」を追加。読む順路・第4節の判断項目・第5.4節「AIに修正を頼む」・第6節のレビュー観点も追加。コード・実行結果は不変)。2026-09-21(Final Content Audit:実行記録が evidence_public に無かったため、コード不変のまま再実行して記録を取得し、第6節の失敗ログの時刻を再実行の値に置き換えた。コード・出力のその他の行は不変)

この記事の会社・人物・データはすべて架空の設定です。実在する会社や案件とは関係ありません。本文の失敗例は、この記事のために意図的に用意した説明用のものであり、実際の業務で観測された障害ではありません。本文のコードはこの記事の作成時に実際にローカルで実行して確認したものです(第7節)。

この記事は何の話か。承認処理で「状態の変更は成功したが、履歴の記録だけが失敗した」という部分失敗が起きたときに、それを正常完了として扱ってしまう説明用のバグを実際に再現し、部分失敗を正しく呼び出し元へ伝えるように直す話です。ただし、この修正で直るのは「失敗を隠さない」ことまでで、「状態だけ変わって履歴が無い」データは残ります。この記事は、それを直した気にならないための記事でもあります。

なぜ必要か。08-02・08-03で作った処理は、途中の1か所が失敗しても全体が止まらずに「成功」と返ってしまうと、記録が欠けたまま業務が進んでしまいます。どこで何が失敗したかを分けて扱う実装が必要です。

この記事を読むとできるようになること。

  • 想定外の入力、部分失敗、再実行、記録、読みやすさを改善する理由を説明できる
  • 部分失敗を正常完了として扱わない修正を加え、修正差分と失敗時の動作例を記録できる
  • ログに残す内容と残さない内容を、再実行の安全性と実装の複雑さを比較して判断できる
  • この修正で直ること・直らないことを分け、直らない部分(不変条件の違反)をどの工程で解決するか言える

読む順路。この記事は3つの深さで読めます。全部を順に読む必要はありません。

  • 基本だけ理解する:第1節→第3節(部分失敗を放置すると何が起きるか、この修正で直ること・直らないこと)→第4節(判断すること)→第6節の対比表→第9節。コードを読まなくても、「失敗を隠さない」と「データを壊さない」が別のことだと分かります。
  • 作って確かめる:上に加えて第5.2〜5.3節(修正前・修正後のコード)→第5.4節(AIに修正を頼む)→第6節(実行結果とレビュー)→第7節。
  • 設計判断を深める:上に加えて第8節(ログの内容、再実行の安全性、不変条件が満たされない理由と記事Bへの引き継ぎ)。

1. 今回の業務課題と完成するもの

この記事でやることは、承認処理で「状態変更は成功し、履歴追加だけが失敗した」という説明用の失敗例を実際に動かして再現し、それを正常完了として扱わないように修正し、修正差分と失敗時の動作例を記録することです。

08-02では状態と履歴を1回の書き込みでまとめて保存する方法を採用し、それが途中失敗への保証ではないことも確認しました。この記事ではあえて別々に書き込む場合に何が起きるかを確かめ、その問題への対処方法の「第一歩」を扱います。

この記事を読み終えると、次のものが手元にできます。

  • 修正前・修正後のコード(examples/08-04-error-handling/)
  • 修正差分・失敗時の動作例ひな形(失敗の場面、修正前の動作、修正後の動作、ログの内容、再実行の扱い)

2. 前提知識と読む順路

この記事で使う言葉を先に説明します。

部分失敗

一言でいうと、複数の処理のうち一部だけが失敗し、残りは成功してしまう状態です。たとえば、状態の変更は保存できたのに、その変更を記録する履歴だけが保存できなかった場合です。これがないと、処理全体を「成功」か「失敗」かの2択でしか考えられず、一部だけ欠けた記録に気づけません。

ログ

一言でいうと、いつ・誰が・何をして・どうなったかを残す記録です。

再実行

一言でいうと、失敗した操作をもう一度やり直すことです。

読む順路:保存処理は08-02、連携処理は08-03、例外時の画面は04-04、状態遷移は05-02、守るべき不変条件は記事Aが該当します。この記事の修正差分は09-04でレビューし、残る問題(第3.3節)は記事B(09-02)のトランザクションで扱います。09-01はこの修正をテスト対象にしていません。

3. 理解する:部分失敗を放置すると何が起きるか

3.1 「動いた」と「正しく記録された」は別のこと

処理が例外を投げずに戻り値を返したからといって、すべての保存が完了したとは限りません。この記事では、状態ファイルへの保存は成功し、履歴ファイルへの保存だけが例外で失敗する場面を作り、その例外を握りつぶしてしまうと何が起きるかを確認します。

3.2 よくある誤解

「例外が発生しても、catchで受け止めていれば安全」。catchで例外を受け止めても、その後に何もせず処理を続け、呼び出し元に成功を返してしまえば、失敗を隠しただけになります(第6節で実際に確認します)。

「ログに全部残しておけば安心」。パスワードや個人情報などの秘密情報までログに残すと、別の問題(情報漏えい)を生みます。何を残し、何を残さないかを決める必要があります(第8.1節)。

「部分失敗を伝えるようにしたので、REQ-007は満たした」。伝えるようになったのは「正常完了として扱わない」という報告の側だけです。保存先には「承認済み(この記事では提出済み)なのに履歴が無い」依頼が残ります(第3.3節)。

3.3 この修正で直ること・直らないこと

結論から言うと、この記事の修正(storage_v2.js)は「失敗を隠さない」までであり、「データを壊さない」には届いていません。

みらい業務サービスの例で言うと、佐藤さんが提出したとき、状態は「提出済み」になったのに履歴に「提出」が残らなかったとします。修正前は「成功」と返るので誰も気づきません。修正後は「部分失敗」と返り、失敗ログにも残るので、運用担当者は気づけます。しかし保存先を見ると、状態は「提出済み」、履歴は空のままです。記事Aの不変条件「承認済みの依頼には承認の履歴が必ず1件以上ある」に照らすと、この依頼はその約束を破った状態で残っています。

観点修正前(v1)修正後(v2、この記事)満たすために必要なこと(この記事の範囲外)
呼び出し元への報告成功(誤り)部分失敗と明示—
失敗の記録なし失敗ログ(対象・操作者・時刻・種別)—
REQ-007「片方だけの結果を正常として扱わない」扱ってしまう正常とは扱わない—
保存先の状態と履歴状態だけ変わり履歴なし同じ(状態だけ変わり履歴なし)状態変更と履歴追加を「全部成功か全部取り消し」にする(トランザクション。記事B)、または状態を元に戻す処理
記事Aの不変条件(承認済み→履歴が必ずある)破れる破れたまま同上

つまり、この記事の後に必要なのは「報告」ではなく「保存先の作り」の改善です。記事Bは同じ問題をトランザクションで解決し、途中で失敗したら状態も履歴も元に戻ることをテストで確かめています(記事BのT6)。この記事はその前段として、「1回の書き込み」(08-02)でも「失敗を伝える」(この記事)でも足りない、と読者が分かる位置にあります。

失敗を伝えても、データは戻らない。修正前:失敗を隠す:状態だけ保存、履歴は失敗、それでも「成功」と返す。修正後:部分失敗を伝える:失敗ログも残す、状態だけ保存されたまま。次に必要な改善:状態と履歴を全部成功か全部取消へ。この記事の修正は「失敗を隠さない」まで。

4. 判断すること

部分失敗の扱いを決めるときに人が判断することは、次の5つです。

判断することなぜ判断が必要か仮置きしてよいか間違えた場合の影響
どの失敗を「部分失敗」として扱うか(状態は書けたが履歴が書けない)失敗の場面を決めないと、AIは「例外を catch する」だけのコードを書き、何が失敗したかを区別しない可(この記事は履歴の書き込み失敗1場面。実際の業務ではディスク不足・通信断なども)別の場面の失敗が「成功」として通る
失敗を呼び出し元にどう伝えるか(例外か、戻り値か)伝え方で、画面(04-04)や処理側(08-03)の作りが変わる可(この記事は戻り値に partial: true。08-03のサーバーへ組み込む形は未実装)呼び出し元が success しか見ず、部分失敗を成功として画面に出す
ログに何を残し、何を残さないか原因調査に要る情報と、残してはいけない情報(秘密情報)が両方ある(第8.1節)不可。残さないものを先に決めるパスワード・個人情報がログに残る
状態変更を取り消す(ロールバック)か、伝えるだけにするか取り消しには「全部成功か全部取り消し」を保証する保存先の仕組みが要る(第3.3節)可(この記事は伝えるだけで仮置きし、取り消す形は記事Bへ)「伝えるようにしたから直った」と誤解し、不変条件が破れたデータが残り続ける
この修正を、どの工程で検証するか修正差分のレビューと、修正後の動作の確認は別可(レビューは09-04、トランザクションでの解決と確認は記事B)「09-01でテストする」と書いたまま、実際には誰も検証しない

勝手に決めてはいけないこと:実際の業務で起きた障害であるかのように書きません。この記事の失敗例は、循環参照データを渡すことで意図的に起こした説明用のものです。ログに残す項目(操作者、時刻、対象、結果の種類)を超えて、秘密情報(パスワードなど)を記録する実装にしません。「部分失敗を伝えた」ことを「REQ-007と不変条件を満たした」と書きません。

AIに任せる範囲:失敗を再現する説明用コードの下書き、失敗を検知して戻り値とログに反映する修正の下書き、実行して出力をそのまま返すことはAIに任せられます。任せてはいけないのは、どの失敗を扱うか、ログに何を残さないか、取り消すか伝えるだけにするか、そして「この修正で何が直っていないか」の判断です。AIの修正案は「例外を catch してログに書く」で「対応済み」と報告しがちで、不変条件が破れたままであることは指摘しません(第6.2節)。

5. 作って確かめる:バグの再現と修正

5.1 準備

08-02の保存処理(状態+履歴を1回の書き込みでまとめる方法)を踏まえ、この記事ではあえて状態と履歴を別々のファイルに書き込む実装を用意します。記事Aの不変条件(承認済みには承認履歴が必ずある)を、確認の基準として手元に置きます。

5.2 修正前のコード(説明用のバグ)

// examples/08-04-error-handling/storage_v1.js
// 教材演習用:意図的に不備を入れた説明用の失敗例。
// 状態変更と履歴追加を別々に書き込み、履歴の書き込みが例外で失敗しても
// 例外を握りつぶして呼び出し元には常に成功として返してしまう(部分失敗を正常完了として扱うバグ)。
const fs = require("node:fs");
const path = require("node:path");
const STATE_FILE = path.join(__dirname, "practice-state.json");
const HISTORY_FILE = path.join(__dirname, "practice-history.json");

function loadJson(file, fallback) {
  if (!fs.existsSync(file)) return fallback;
  return JSON.parse(fs.readFileSync(file, "utf8"));
}

function submitRequest(id, actor, historyExtra) {
  const state = loadJson(STATE_FILE, {});
  state[id] = "提出済み";
  fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2)); // ここは成功する

  const history = loadJson(HISTORY_FILE, []);
  const entry = { requestId: id, action: "提出", actor, at: new Date().toISOString(), extra: historyExtra };
  try {
    history.push(entry);
    fs.writeFileSync(HISTORY_FILE, JSON.stringify(history, null, 2)); // extraが循環参照だとここで例外
  } catch (e) {
    console.log("(内部)履歴の書き込みで例外:", e.message);
    // バグ: ここで例外を握りつぶし、呼び出し元には常に成功として返している
  }
  return { success: true, state: "提出済み" };
}

module.exports = { submitRequest, STATE_FILE, HISTORY_FILE, loadJson };

5.3 修正後のコード

// examples/08-04-error-handling/storage_v2.js
// 修正版:履歴の書き込みが失敗した場合、状態変更が完了していても
// 呼び出し元には部分失敗として返し、失敗の記録を別のログに残す。
const fs = require("node:fs");
const path = require("node:path");
const STATE_FILE = path.join(__dirname, "practice-state.json");
const HISTORY_FILE = path.join(__dirname, "practice-history.json");
const FAIL_LOG_FILE = path.join(__dirname, "practice-fail-log.json");

function loadJson(file, fallback) {
  if (!fs.existsSync(file)) return fallback;
  return JSON.parse(fs.readFileSync(file, "utf8"));
}

function appendFailLog(record) {
  const log = loadJson(FAIL_LOG_FILE, []);
  log.push(record);
  fs.writeFileSync(FAIL_LOG_FILE, JSON.stringify(log, null, 2));
}

function submitRequest(id, actor, historyExtra) {
  const state = loadJson(STATE_FILE, {});
  state[id] = "提出済み";
  fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));

  const history = loadJson(HISTORY_FILE, []);
  const entry = { requestId: id, action: "提出", actor, at: new Date().toISOString(), extra: historyExtra };
  try {
    history.push(entry);
    fs.writeFileSync(HISTORY_FILE, JSON.stringify(history, null, 2));
  } catch (e) {
    // 修正: 例外を握りつぶさず、操作者・対象ID・時刻・エラー種別だけをログに残し
    // (秘密情報は残さない)、呼び出し元には部分失敗であることを明示的に伝える。
    appendFailLog({
      requestId: id,
      actor,
      at: new Date().toISOString(),
      errorType: e.constructor.name,
      note: "状態は提出済みに変更されたが、履歴の記録に失敗した",
    });
    return {
      success: false,
      state: "提出済み",
      partial: true,
      error: "履歴の記録に失敗しました。状態は変更されていますが履歴が残っていません。運用担当者へ連絡してください。",
    };
  }
  return { success: true, state: "提出済み" };
}

module.exports = { submitRequest, STATE_FILE, HISTORY_FILE, FAIL_LOG_FILE, loadJson };

5.4 AIに修正を頼む

修正前のコード(第5.2節)を渡して、修正をAIに頼む想定です。渡す情報と渡さない情報を先に決めます。

渡す情報内容渡さない情報と理由
修正前のコードと再現手順storage_v1.js、run_v1.js と、その実行結果(戻り値が成功なのに履歴が空)—
完成条件履歴の書き込みが失敗したら、戻り値で部分失敗を明示し、対象・操作者・時刻・エラー種別を失敗ログに残す。秘密情報は残さない—
範囲の制限状態変更の取り消し(ロールバック)はこの記事では実装しない。伝えるだけにする実データ・実際のログ(架空の入力だけ)

指示文は次のとおりです。これは説明のために書いた例で、実際にAIへ送信した記録ではありません。

storage_v1.js(承認処理の保存部分)に、部分失敗を正常完了として扱うバグがあります。run_v1.js を実行すると、履歴の書き込みが例外で失敗しても戻り値が { success: true } になり、履歴ファイルは空のままです。

修正の完成条件:
- 履歴の書き込みが失敗したら、戻り値を { success: false, partial: true } とし、利用者向けの文言(内部のエラーメッセージを含まない)を error に入れる
- 失敗ログに、対象ID・操作者・時刻・エラー種別・状況の説明だけを残す。秘密情報・個人情報の内容は残さない
- 状態ファイルへの変更は、この修正では元に戻さない(取り消しは別の工程で扱う)

返し方:
- 修正後のコード(storage_v2.js)と、同じ失敗例で動かす run_v2.js を返してください。
- run_v2.js の実行出力をそのまま貼ってください。
- 修正後に「何が直っていないか」も書いてください。

してはいけないこと:
- 例外を握りつぶして成功を返す形を残さないでください。
- 完成条件に無い機能(自動再実行、ロールバック)を追加しないでください。

6. 実行結果(修正差分・失敗時の動作例)

6.1 実行結果

説明用の失敗例として、履歴データに循環参照(自分自身を含むデータ)を渡し、履歴の書き込みだけを失敗させます(実データではありえない想定の失敗例です)。

$ node run_v1.js
=== 修正前(storage_v1.js):説明用の失敗例 ===
履歴に循環参照を含むデータを渡し、履歴の書き込みだけを失敗させる(実データではありえない想定の失敗例)
(内部)履歴の書き込みで例外: Converting circular structure to JSON
    --> starting at object with constructor 'Object'
    --- property 'self' closes the circle
submitRequestの戻り値: {"success":true,"state":"提出済み"}
=== 実際のファイルの中身を確認する ===
状態ファイル: {"REQ-0003":"提出済み"}
履歴ファイル: []
$ node run_v2.js
=== 修正後(storage_v2.js):同じ失敗例で動作を確認する ===
submitRequestの戻り値: {"success":false,"state":"提出済み","partial":true,"error":"履歴の記録に失敗しました。状態は変更されていますが履歴が残っていません。運用担当者へ連絡してください。"}
=== 実際のファイルの中身を確認する ===
状態ファイル: {"REQ-0003":"提出済み"}
履歴ファイル: []
失敗ログ: [{"requestId":"REQ-0003","actor":"佐藤","at":"2026-09-21T01:34:31.296Z","errorType":"TypeError","note":"状態は提出済みに変更されたが、履歴の記録に失敗した"}]
項目修正前(storage_v1.js)修正後(storage_v2.js)
失敗の場面履歴の書き込みで例外が発生同左
状態ファイルへの反映「提出済み」に変更される同左(変更は元に戻していない)
呼び出し元への戻り値success: true(誤った成功)success: false, partial: true(部分失敗を明示)
ログの内容なし(例外を握りつぶす)対象ID・操作者・時刻・エラー種別・状況の説明(失敗ログに記録)
再実行の扱い呼び出し元は失敗に気づけないため再実行されない呼び出し元がpartial: trueを見て再実行または運用担当への連絡を判断できる
保存先の状態と履歴(不変条件)状態「提出済み」・履歴なし(不変条件を破る)同じ(状態「提出済み」・履歴なし。不変条件は破れたまま)

6.2 修正後のコードをレビューする

修正後のコードを「直った」と受け取る前に、完成条件と、完成条件に書いていないこと(不変条件)の両方で見ます。

観点何を見るか結果誰が判断するか
完成条件:戻り値失敗時に success: false, partial: true になるか満たす(第6.1節の run_v2.js)開発担当
完成条件:ログ対象・操作者・時刻・種別だけか。秘密情報が無いか満たす。extra(循環参照データ)はログに入れていない開発担当
完成条件に無いこと:保存先修正後に状態ファイルと履歴ファイルを見て、記事Aの不変条件(承認済み→履歴が必ずある)を満たすか満たさない。状態「提出済み」・履歴 [] のまま(第6.1節の「実際のファイルの中身」)開発担当と、要件の責任者
「直っていないこと」の申告AIの報告に「状態は元に戻していない」が含まれるか指示文で求めたため含まれる想定。含まれていなければ人が足す開発担当

見つけ方は、戻り値やログだけを見ず、実際のファイルの中身を見ることです。第6.1節の出力の「実際のファイルの中身を確認する」以下が、修正前後で同じであることが、この修正の限界を示しています。「テストが通った」「ログに残った」だけでは、この行は見えません。

この結果を受けて、この記事では次のように判断します。修正後のコードは「失敗を隠さない」修正として採用する。「状態だけ変わって履歴が無い」問題は未解決のまま、09-04のレビューで残課題として記録し、解決は記事Bのトランザクションに委ねる(第8.3節)。

7. 確認結果

実行環境:macOS 26.6.2、Node.js v24.18.0(node:fs、node:pathのみ使用)、2026-09-20に実行(stdout・stderr・終了コードをそのまま記録した実行記録は2026-09-21の再実行で取得。第9節の run_log.md)。教材演習用のローカルファイルのみを使い、外部のネットワークやデータベースには接続していません。

完了条件との照合:

完了条件確認結果
部分失敗の例と修正後の動作が対になっている第6節の表に記載
説明用の失敗例と実際に観測した失敗が区別されている冒頭の注記、本節、第11節に記載
ログに残す項目と残さない項目がある第8.1節に記載、失敗ログの内容は第6節に記載

残る制限:この失敗例は、循環参照データを渡すことで意図的に起こしたものです。実際の業務で起きうる部分失敗(ディスク容量不足、通信断など)を再現したものではありません。修正後も、保存先には状態だけ変わって履歴が無い依頼が残り、記事Aの不変条件は満たしていません(第3.3節・第6.2節)。第5.4節の指示文は説明用の例であり、実際にAIへ送信・観測した記録ではありません。この記事の本文はClaude Codeが作成したもので、業務担当者による内容確認は行っていません。

8. 設計を判断する

8.1 ログに残す内容と残さない内容

区分内容この記事での扱い
残す操作者、時刻、対象ID、結果の種類(成功・部分失敗・失敗)、エラー種別失敗ログに記録する(第5.3節)
残さないパスワード、個人情報の内容そのもの、秘密情報この記事の失敗ログには含めていない

ログに何でも残せば安全というわけではありません。原因調査に必要な項目に絞り、秘密情報は残さないという判断が必要です。

8.2 再実行の安全性と実装の複雑さを比較する

選択肢内容適する場合
何もしない(この記事の修正前)部分失敗を検知しない適する場合はない(この記事で問題点として扱う)
部分失敗を伝えるだけ(この記事で採用)呼び出し元にpartial: trueを返し、判断を委ねる再実行の条件が複雑で、自動化のリスクが大きい場合
自動で再実行する履歴の書き込みだけを自動的にやり直す再実行しても副作用が起きないことが保証できる場合

この記事の教材演習では、実装の複雑さを抑えるため、部分失敗を伝えるだけの方法を採用しています。自動での再実行は、再実行しても状態が二重に変わらないことを保証する設計が別途必要で、この記事では扱っていません。

8.3 不変条件が満たされない理由と、記事Bへ引き継ぐもの

この修正は、履歴の書き込み失敗を検知して呼び出し元に伝えるところまでです。状態変更そのものを取り消す(ロールバックする)処理は行っておらず、状態は「提出済み」のまま残ります。

不変条件が満たされない理由は、状態と履歴を別々のファイルに、別々のタイミングで書いているからです。2回に分けて書く限り、1回目と2回目の間で失敗する隙は消えません。08-02の「1回の書き込み」は隙を小さくしますが、書き込み途中の失敗や同時書き込みには効きません。根本的には、「状態変更と履歴追加を、全部成功か全部取り消しのどちらかにする」仕組みが保存先に要ります。

引き継ぐもの引き継ぎ先そこで何が起きるか
「状態だけ変わって履歴が無い」が起こり得るという事実と、この記事の再現手順09-04修正差分のレビューで「状態のロールバック」を未対応の残課題として記録する
不変条件を保存先で守る設計(トランザクション)と、途中失敗のテスト記事B(09-02)状態変更と履歴追加を1つのトランザクションにし、途中失敗(T6、AC-010)で状態も履歴も元に戻ることを確かめる
部分失敗を運用でどう検知し、誰が対処するか12-02(異常時の初動)、10-04(利用開始判定の未解決問題)失敗ログの監視と、発生時の対応手順。この教材の題材では「演習の範囲で受容、限定試行前に扱いを決める」として扱われている

なぜ次のVerify工程が必要か:この記事の run_v2.js は「修正版が部分失敗を伝えること」を確かめますが、「不変条件が守られること」は確かめていません(守られていないので、確かめれば失敗します)。不変条件を守る実装とそのテストは記事Bにあり、この記事の題材アプリ(JSONファイル保存)にその設計を適用するかどうかは、この教材の時点では未決です。

8.4 適用限界

この修正は、教材演習用のファイル2つへの書き込みという小さな例に対するものです。実際の保存先(データベース)では、トランザクションの有無と設定によって、同じ失敗の見え方も対処も変わります。

9. 持ち帰る成果物

成果物空欄版記入例
修正差分・失敗時の動作例ひな形(失敗の場面、修正前の動作、修正後の動作、ログの内容、再実行の扱い)templates/08-04-error-handling_template.mdtemplates/08-04-error-handling_example.md
修正前・修正後コード一式examples/08-04-error-handling/(storage_v1.js、storage_v2.js、run_v1.js、run_v2.js)同上
実行記録(stdout・stderr・終了コード、整形なし)—evidence_public/final-audit-2026-09-21/08-04/run_log.md

自分の業務に使うときの手順は次のとおりです。

  1. 状態変更を含む処理のうち、複数の保存先に分かれている箇所を洗い出す
  2. 一部だけ失敗する場面を想定し、説明用の失敗例で再現する
  3. 失敗を握りつぶさず、呼び出し元に部分失敗として伝える修正を加える
  4. ログに残す項目と残さない項目を決め、修正差分と動作例を記録する
  5. 修正後に実際の保存先の中身を見て、「直ったこと」と「直っていないこと」を分けて記録し、直っていないことの引き継ぎ先を書く

10. 次工程への受渡し

次へ渡すもの:修正前・修正後のコード、修正差分・失敗時の動作例、直っていないこと(状態だけ変わって履歴が無い依頼が残る)とその引き継ぎ先(第8.3節)。

次工程での使われ方:09-04では、この修正差分をレビューし、状態のロールバックを残課題として記録します。記事B(09-02)では、同じ問題をトランザクションで解決し、途中失敗のテスト(T6)で確かめます。09-03では途中失敗の結果をこの記事から引用します。09-01は08-01の判定関数だけを対象にしており、この修正はテスト対象ではありません。

前工程へ戻る条件:この修正が04-04の例外画面や05-02の状態遷移と食い違う場合は、それぞれの記事に戻って確認します。

11. 根拠と更新情報

  • この記事の会社・人物・データはすべて架空の設定です。
  • 状態遷移の考え方は05-02、例外時の画面は04-04に基づきます。
  • 制作の来歴:本文はClaude Codeが作成しました。第5〜7節のコードは、2026-09-20にmacOS 26.6.2・Node.js v24.18.0の環境で実際に実行し、結果を確認しています。人(ユーザー)による実読・内容確認は未実施です。
  • 不変条件「承認済みの依頼には承認の履歴が必ず1件以上ある」は記事A、トランザクションによる解決と途中失敗のテストは記事Bに基づきます。
  • 第5.4節の指示文は、この記事のために書いた指示文の例であり、実際にAIへ送信した記録ではありません。
  • 2026-09-20(Phase 5B-2B):受入監査で「修正版でも状態だけ変わって履歴が残らず、記事Aの不変条件を満たさない。記事Bのトランザクションとの関係が示されていない」と指摘された。第3.3節「この修正で直ること・直らないこと」、第6.2節「修正後のコードをレビューする」、第8.3節「不変条件が満たされない理由と、記事Bへ引き継ぐもの」を追加し、第10節の引き継ぎ先を09-04・記事B・09-03に改めた(09-01はこの修正を対象にしないことを明記)。あわせて読む順路、第4節の判断項目、第5.4節「AIに修正を頼む」、CHECKLIST、記事間の参照を追加した。コード・実行結果は変えていない。
  • 2026-09-21(Final Content Audit):第6節の実行結果に対応する実行記録(stdout・stderr・終了コード)が evidence_public/ に無かったため、コードを変えずに再実行して evidence_public/final-audit-2026-09-21/08-04/run_log.md に記録した。失敗ログの時刻以外の出力は初版と同一。
  • 未確認事項:実際の業務で起きうる部分失敗(ディスク容量不足、通信断など)の再現、状態変更のロールバック(取り消し)の実装、題材アプリへのトランザクションの適用は、この記事の時点では行っていません。