背景
最近、Next.js + Nest.jsを使ったTypescriptのサーバーサイド構成のプロジェクトで、
バリデーションやTryCatchの際にconsole.errorを仕込み、CloudWatchでログ監視を導入しました。
ところがいざ動かしてみると、
エラー1件が何十行にも分割されていて、CloudWatchのタイムラインが異常に長くなってしまいました。
例えば404エラーが発生した場合を例にしましょう。
下記のようなJsonオブジェクトを期待するのですが、何行にも渡ってしまい、非常に見にくい。
期待する出力エラーオブジェクト
{
"message": "xxx",
"error": "Not Found",
"statusCode": 404,
"log": "log"
}
使いどころ
const response = async (): Promise<Response | undefined> => {
try {
const res = await fetch("https://example.com/api");
return res;
} catch (error) {
console.error("Fetch error:", error);
}
};
CloudWathに実際に出力されたログ
特にログが起こった時間を手がかりにデバッグしたいとき、
ログ1件に対して、同じ時刻のログが複数行発生してしまいました。
エラーの原因を追うどころか、まず見つけるのが大変という状態になってしまいます。
これは時間の一意性のないログは運用上、かなり不便です。
致命的エラーの場合、サービス復旧の時間にも影響してきます。
問題:CloudWatchでは改行が分割される
CloudWatch Logsは、改行ごとに別イベントとして扱われる仕様です。
つまり、console.error(error) の出力が複数行になると、それぞれ独立したログとして記録されます。
try {
throw new Error("Not found");
} catch (error) {
console.error("Not found error:", error);
}
CloudWatch上ではこんな感じで、一つのStringとして出力されます。
Session validation error: Error: No valid PHPSESSID found
at AuthService.validate (/app/auth.service.ts:42:13)
at ...
改行が全て別レコード扱いになり、1つのエラーがバラバラになってしまうのです。
TSでJSON.stringify(error)しても{}しか出ない
そこで一見スマートに見えるこの方法を試しました。
console.error("error",JSON.stringify(error));
…が、結果は errror:{}というふうに空のオブジェクトが出力されました。
なぜ?
これはJavaScriptの仕様によるものです。
JavaScriptのErrorオブジェクトは message / stack / name で構成されるオブジェクトです。
これらはすべて、**「列挙不可(non-enumerable)」**という設定が暗黙的にされています。
non-enumerableとして扱われるプロパティは、キーを指定しても値が参照できないオブジェクトになっています。
const err = new Error("something went wrong");
console.log(Object.keys(err)); // []
console.log(JSON.stringify(err)); // {}
JSON.stringify() は「列挙可能なプロパティ」しか出力できないので、結果的に {} しか残りません。
PythonやJavaのExceptionオブジェクトはこのように暗黙的に参照できない形になっておらず、JavaScriptの特徴のようです。
解決策:Errorだけ特別扱いして1行JSON化する
そこでErrorオブジェクトだけを特別扱いし、
JSON化しても情報が失われないsafeJsonStringify()を実装しました。
export function safeJsonStringify(
obj: unknown
): string | { name: string; message: string; stack?: string } {
try {
if (obj instanceof Error) {
return {
name: obj.name,
message: obj.message,
stack: obj.stack,
};
}
return JSON.stringify(obj);
} catch {
return String(obj);
}
}
使用例:
try {
// API呼び出しなど
} catch (error) {
console.error("API call failed:", safeJsonStringify(error));
}
CloudWatch出力:
API call failed: {"name":"Error","message":"Not found","stack":"Error: ..."}
実際の出力画面
1行JSONで出力されるため、CloudWatchのタイムラインがすっきりし、
Logs Insightsでも検索・集計しやすくなります。
🪄 まとめ
-
console.error(error)はCloudWatchで改行が分割される -
JSON.stringify(error)は{}になる(JS特有のnon-enumerableが原因) -
Errorを明示的に展開してJSON化するのがベスト - 1行出力にするだけで、デバッグ効率が上がる!
終わりに
「ログ整形」は地味だけど、運用の快適さを大きく変えます。
一時が万事というやつです。
console.errorのErrorオブジェクトはクライアント側でそこまで意識していなかったのですが、Serverサイドは少し事情が違うので気をつけましょう。
CloudWatchのタイムラインがすっきりするだけで、デバッグ効率が何倍にも上がり、チームでも運用しやすくなりました。

