[{"data":1,"prerenderedAt":1990},["ShallowReactive",2],{"en-post-\u002Fen\u002Fapi-error-response-format":3},{"id":4,"title":5,"body":6,"description":1977,"extension":1978,"meta":1979,"navigation":49,"path":1985,"seo":1986,"sitemap":1987,"stem":1988,"__hash__":1989},"blogEn\u002Fen\u002Fapi-error-response-format.md","Designing API Error Responses: Unifying Inconsistent Errors with RFC 9457 Problem Details",{"type":7,"value":8,"toc":1968},"minimark",[9,14,18,150,164,185,188,192,295,314,316,320,327,335,408,432,448,450,454,460,630,646,648,652,659,1150,1156,1422,1425,1510,1521,1523,1527,1530,1862,1868,1870,1874,1961,1964],[10,11,13],"h2",{"id":12},"when-every-error-has-a-different-shape","When every error has a different shape",[15,16,17],"p",{},"Say the APIs in one service return errors like these:",[19,20,25],"pre",{"className":21,"code":22,"language":23,"meta":24,"style":24},"language-js shiki shiki-themes github-light github-dark","\u002F\u002F Login API\n\"Wrong password\"\n\n\u002F\u002F Sign-up API\n{ \"error\": \"duplicate email\" }\n\n\u002F\u002F Posts API\n{ \"success\": false, \"msg\": \"no permission\", \"errCode\": 1003 }\n\n\u002F\u002F Payments API\n{ \"message\": \"Error: Cannot read properties of undefined (reading 'card')\" }\n","js","",[26,27,28,37,44,51,57,76,81,87,124,129,135],"code",{"__ignoreMap":24},[29,30,33],"span",{"class":31,"line":32},"line",1,[29,34,36],{"class":35},"sJ8bj","\u002F\u002F Login API\n",[29,38,40],{"class":31,"line":39},2,[29,41,43],{"class":42},"sZZnC","\"Wrong password\"\n",[29,45,47],{"class":31,"line":46},3,[29,48,50],{"emptyLinePlaceholder":49},true,"\n",[29,52,54],{"class":31,"line":53},4,[29,55,56],{"class":35},"\u002F\u002F Sign-up API\n",[29,58,60,64,67,70,73],{"class":31,"line":59},5,[29,61,63],{"class":62},"sVt8B","{ ",[29,65,66],{"class":42},"\"error\"",[29,68,69],{"class":62},": ",[29,71,72],{"class":42},"\"duplicate email\"",[29,74,75],{"class":62}," }\n",[29,77,79],{"class":31,"line":78},6,[29,80,50],{"emptyLinePlaceholder":49},[29,82,84],{"class":31,"line":83},7,[29,85,86],{"class":35},"\u002F\u002F Posts API\n",[29,88,90,92,95,97,101,104,107,109,112,114,117,119,122],{"class":31,"line":89},8,[29,91,63],{"class":62},[29,93,94],{"class":42},"\"success\"",[29,96,69],{"class":62},[29,98,100],{"class":99},"sj4cs","false",[29,102,103],{"class":62},", ",[29,105,106],{"class":42},"\"msg\"",[29,108,69],{"class":62},[29,110,111],{"class":42},"\"no permission\"",[29,113,103],{"class":62},[29,115,116],{"class":42},"\"errCode\"",[29,118,69],{"class":62},[29,120,121],{"class":99},"1003",[29,123,75],{"class":62},[29,125,127],{"class":31,"line":126},9,[29,128,50],{"emptyLinePlaceholder":49},[29,130,132],{"class":31,"line":131},10,[29,133,134],{"class":35},"\u002F\u002F Payments API\n",[29,136,138,140,143,145,148],{"class":31,"line":137},11,[29,139,63],{"class":62},[29,141,142],{"class":42},"\"message\"",[29,144,69],{"class":62},[29,146,147],{"class":42},"\"Error: Cannot read properties of undefined (reading 'card')\"",[29,149,75],{"class":62},[15,151,152,153,158,159,163],{},"The frontend needs separate code to pull the error out of each API, and some errors are internal messages you can't show to users. Once ",[154,155,157],"a",{"href":156},"\u002Fen\u002Fhttp-status-codes-guide","HTTP status codes"," tell clients \"what kind of failure\" happened, it's the ",[160,161,162],"strong",{},"body's"," turn to say \"what failed, why, and how\" in a consistent shape.",[165,166,167],"blockquote",{},[15,168,169,170,173,174,177,178,181,182],{},"A good error response has ",[160,171,172],{},"the same shape across every API",", carries both ",[160,175,176],{},"a code programs can branch on"," and ",[160,179,180],{},"a message people can read",", and ",[160,183,184],{},"leaves out internals.",[186,187],"hr",{},[10,189,191],{"id":190},"what-makes-a-good-error-response","What makes a good error response",[193,194,195,211],"table",{},[196,197,198],"thead",{},[199,200,201,205,208],"tr",{},[202,203,204],"th",{},"Property",[202,206,207],{},"Why it matters",[202,209,210],{},"Example",[212,213,214,229,242,255,268,281],"tbody",{},[199,215,216,220,223],{},[217,218,219],"td",{},"Same structure in every API",[217,221,222],{},"One piece of frontend error handling covers every API",[217,224,225,226],{},"Always ",[26,227,228],{},"{ type, title, status, detail, ... }",[199,230,231,234,237],{},[217,232,233],{},"Machine-readable code",[217,235,236],{},"Branching logic doesn't break when the wording changes",[217,238,239],{},[26,240,241],{},"\"code\": \"email_taken\"",[199,243,244,247,250],{},[217,245,246],{},"Human-readable message",[217,248,249],{},"Developer debugging, and text safe to show as-is",[217,251,252],{},[26,253,254],{},"\"detail\": \"This email is already registered.\"",[199,256,257,260,263],{},[217,258,259],{},"Per-field error list",[217,261,262],{},"Forms can mark exactly which inputs are wrong",[217,264,265],{},[26,266,267],{},"\"errors\": [{ \"field\": \"email\", ... }]",[199,269,270,273,276],{},[217,271,272],{},"Trace ID",[217,274,275],{},"When a user reports a problem, you can find it in the server logs immediately",[217,277,278],{},[26,279,280],{},"\"traceId\": \"8f2c...\"",[199,282,283,286,289],{},[217,284,285],{},"No internals",[217,287,288],{},"Stack traces, SQL, and internal IPs are clues for attackers",[217,290,291,292],{},"❌ ",[26,293,294],{},"\"Cannot read properties of undefined\"",[296,297,298],"warning",{},[165,299,300],{},[15,301,302,303,306,307,310,311,313],{},"Don't make the frontend branch on ",[160,304,305],{},"message strings",". Code like ",[26,308,309],{},"if (error.message === \"This email is already registered.\")"," breaks the moment someone rewords or translates the message. Always branch on a stable ",[26,312,26],{},".",[186,315],{},[10,317,319],{"id":318},"theres-a-standard-rfc-9457-problem-details","There's a standard: RFC 9457 Problem Details",[15,321,322,323,326],{},"You don't need to invent an error format from scratch. There's a standard for HTTP API error responses: ",[160,324,325],{},"RFC 9457 (Problem Details for HTTP APIs)",", which replaced the earlier RFC 7807 in 2023.",[19,328,333],{"className":329,"code":331,"language":332},[330],"language-text","HTTP\u002F1.1 409 Conflict\nContent-Type: application\u002Fproblem+json\n\n{\n  \"type\": \"https:\u002F\u002Fapi.example.com\u002Ferrors\u002Femail_taken\",\n  \"title\": \"Email already in use\",\n  \"status\": 409,\n  \"detail\": \"taken@example.com is already registered.\",\n  \"instance\": \"\u002Fusers\"\n}\n","text",[26,334,331],{"__ignoreMap":24},[193,336,337,347],{},[196,338,339],{},[199,340,341,344],{},[202,342,343],{},"Field",[202,345,346],{},"Meaning",[212,348,349,363,375,385,398],{},[199,350,351,356],{},[217,352,353],{},[26,354,355],{},"type",[217,357,358,359,362],{},"A URI identifying the kind of error. Ideally it points to a page documenting it (",[26,360,361],{},"\"about:blank\""," if none)",[199,364,365,370],{},[217,366,367],{},[26,368,369],{},"title",[217,371,372,373],{},"A short summary of the error kind. Always the same text for the same ",[26,374,355],{},[199,376,377,382],{},[217,378,379],{},[26,380,381],{},"status",[217,383,384],{},"The HTTP status code (matching the response's status)",[199,386,387,392],{},[217,388,389],{},[26,390,391],{},"detail",[217,393,394,395],{},"A specific explanation of ",[160,396,397],{},"this occurrence",[199,399,400,405],{},[217,401,402],{},[26,403,404],{},"instance",[217,406,407],{},"The request path or identifier where the problem occurred",[15,409,410,411,414,415,418,419,422,423,103,425,181,428,431],{},"Use ",[26,412,413],{},"application\u002Fproblem+json"," as the ",[26,416,417],{},"Content-Type",". Beyond the standard fields you can freely add ",[160,420,421],{},"extension members",", so fields like ",[26,424,26],{},[26,426,427],{},"errors",[26,429,430],{},"traceId"," are fine.",[433,434,435],"tip",{},[165,436,437],{},[15,438,439,440,443,444,447],{},"Major frameworks support this format out of the box, like Spring's (Java) ",[26,441,442],{},"ProblemDetail"," and ASP.NET Core's ",[26,445,446],{},"ProblemDetails",". Following the standard instead of inventing an in-house convention means other teams and outside developers understand it immediately.",[186,449],{},[10,451,453],{"id":452},"validation-errors-report-them-per-field","Validation errors: report them per field",[15,455,456,457,313],{},"Several fields can fail validation at once. If you only return the first error, users get stuck in a loop of fixing, submitting, and seeing the next error. Report ",[160,458,459],{},"every invalid field at once",[19,461,465],{"className":462,"code":463,"language":464,"meta":24,"style":24},"language-json shiki shiki-themes github-light github-dark","{\n  \"type\": \"https:\u002F\u002Fapi.example.com\u002Ferrors\u002Fvalidation_failed\",\n  \"title\": \"Invalid input\",\n  \"status\": 422,\n  \"detail\": \"Please check 2 fields.\",\n  \"instance\": \"\u002Fusers\",\n  \"code\": \"validation_failed\",\n  \"errors\": [\n    { \"field\": \"email\", \"code\": \"invalid_format\", \"message\": \"Email format is invalid.\" },\n    { \"field\": \"password\", \"code\": \"too_short\", \"message\": \"Password must be at least 8 characters.\" }\n  ]\n}\n","json",[26,466,467,472,485,497,509,521,533,545,553,588,619,624],{"__ignoreMap":24},[29,468,469],{"class":31,"line":32},[29,470,471],{"class":62},"{\n",[29,473,474,477,479,482],{"class":31,"line":39},[29,475,476],{"class":99},"  \"type\"",[29,478,69],{"class":62},[29,480,481],{"class":42},"\"https:\u002F\u002Fapi.example.com\u002Ferrors\u002Fvalidation_failed\"",[29,483,484],{"class":62},",\n",[29,486,487,490,492,495],{"class":31,"line":46},[29,488,489],{"class":99},"  \"title\"",[29,491,69],{"class":62},[29,493,494],{"class":42},"\"Invalid input\"",[29,496,484],{"class":62},[29,498,499,502,504,507],{"class":31,"line":53},[29,500,501],{"class":99},"  \"status\"",[29,503,69],{"class":62},[29,505,506],{"class":99},"422",[29,508,484],{"class":62},[29,510,511,514,516,519],{"class":31,"line":59},[29,512,513],{"class":99},"  \"detail\"",[29,515,69],{"class":62},[29,517,518],{"class":42},"\"Please check 2 fields.\"",[29,520,484],{"class":62},[29,522,523,526,528,531],{"class":31,"line":78},[29,524,525],{"class":99},"  \"instance\"",[29,527,69],{"class":62},[29,529,530],{"class":42},"\"\u002Fusers\"",[29,532,484],{"class":62},[29,534,535,538,540,543],{"class":31,"line":83},[29,536,537],{"class":99},"  \"code\"",[29,539,69],{"class":62},[29,541,542],{"class":42},"\"validation_failed\"",[29,544,484],{"class":62},[29,546,547,550],{"class":31,"line":89},[29,548,549],{"class":99},"  \"errors\"",[29,551,552],{"class":62},": [\n",[29,554,555,558,561,563,566,568,571,573,576,578,580,582,585],{"class":31,"line":126},[29,556,557],{"class":62},"    { ",[29,559,560],{"class":99},"\"field\"",[29,562,69],{"class":62},[29,564,565],{"class":42},"\"email\"",[29,567,103],{"class":62},[29,569,570],{"class":99},"\"code\"",[29,572,69],{"class":62},[29,574,575],{"class":42},"\"invalid_format\"",[29,577,103],{"class":62},[29,579,142],{"class":99},[29,581,69],{"class":62},[29,583,584],{"class":42},"\"Email format is invalid.\"",[29,586,587],{"class":62}," },\n",[29,589,590,592,594,596,599,601,603,605,608,610,612,614,617],{"class":31,"line":131},[29,591,557],{"class":62},[29,593,560],{"class":99},[29,595,69],{"class":62},[29,597,598],{"class":42},"\"password\"",[29,600,103],{"class":62},[29,602,570],{"class":99},[29,604,69],{"class":62},[29,606,607],{"class":42},"\"too_short\"",[29,609,103],{"class":62},[29,611,142],{"class":99},[29,613,69],{"class":62},[29,615,616],{"class":42},"\"Password must be at least 8 characters.\"",[29,618,75],{"class":62},[29,620,621],{"class":31,"line":137},[29,622,623],{"class":62},"  ]\n",[29,625,627],{"class":31,"line":626},12,[29,628,629],{"class":62},"}\n",[15,631,632,633,635,636,638,639,642,643,313],{},"The frontend can loop over ",[26,634,427],{}," and show each message under its input. For a multilingual service, use ",[26,637,26],{}," (",[26,640,641],{},"too_short",") as the translation key instead of ",[26,644,645],{},"message",[186,647],{},[10,649,651],{"id":650},"handle-it-in-one-place-on-the-server-nodejs","Handle it in one place on the server (Node.js)",[15,653,654,655,658],{},"If every API builds its own error responses, the shapes drift apart. Keep ",[160,656,657],{},"one error class and one function that sends error responses",", and route every error through them.",[19,660,662],{"className":21,"code":661,"language":23,"meta":24,"style":24},"import { randomUUID } from \"node:crypto\";\n\n\u002F\u002F 1. A class for expected errors (ones it's safe to tell the client about)\nclass ApiError extends Error {\n  constructor(status, code, title, detail, extra = {}) {\n    super(detail);\n    this.status = status;\n    this.code = code;\n    this.title = title;\n    this.extra = extra;\n  }\n}\n\n\u002F\u002F 2. The single place that turns any error into Problem Details\nfunction sendProblem(req, res, err) {\n  const traceId = randomUUID();\n\n  let body;\n  if (err instanceof ApiError) {\n    body = {\n      type: `https:\u002F\u002Fapi.example.com\u002Ferrors\u002F${err.code}`,\n      title: err.title,\n      status: err.status,\n      detail: err.message,\n      instance: req.url,\n      code: err.code,\n      traceId,\n      ...err.extra,\n    };\n  } else {\n    \u002F\u002F Unexpected error: keep the details in the server log only\n    console.error(`[${traceId}]`, err);\n    body = {\n      type: \"about:blank\",\n      title: \"Internal Server Error\",\n      status: 500,\n      detail: \"Something went wrong while processing the request.\",\n      instance: req.url,\n      traceId,\n    };\n  }\n\n  res.statusCode = body.status;\n  res.setHeader(\"Content-Type\", \"application\u002Fproblem+json\");\n  res.end(JSON.stringify(body));\n}\n",[26,663,664,682,686,691,709,743,751,765,777,789,801,806,810,815,821,848,865,870,879,895,905,925,931,937,943,949,955,961,970,976,987,993,1015,1024,1033,1044,1055,1066,1071,1076,1081,1086,1091,1102,1124,1145],{"__ignoreMap":24},[29,665,666,670,673,676,679],{"class":31,"line":32},[29,667,669],{"class":668},"szBVR","import",[29,671,672],{"class":62}," { randomUUID } ",[29,674,675],{"class":668},"from",[29,677,678],{"class":42}," \"node:crypto\"",[29,680,681],{"class":62},";\n",[29,683,684],{"class":31,"line":39},[29,685,50],{"emptyLinePlaceholder":49},[29,687,688],{"class":31,"line":46},[29,689,690],{"class":35},"\u002F\u002F 1. A class for expected errors (ones it's safe to tell the client about)\n",[29,692,693,696,700,703,706],{"class":31,"line":53},[29,694,695],{"class":668},"class",[29,697,699],{"class":698},"sScJk"," ApiError",[29,701,702],{"class":668}," extends",[29,704,705],{"class":698}," Error",[29,707,708],{"class":62}," {\n",[29,710,711,714,717,720,722,724,726,728,730,732,734,737,740],{"class":31,"line":59},[29,712,713],{"class":668},"  constructor",[29,715,716],{"class":62},"(",[29,718,381],{"class":719},"s4XuR",[29,721,103],{"class":62},[29,723,26],{"class":719},[29,725,103],{"class":62},[29,727,369],{"class":719},[29,729,103],{"class":62},[29,731,391],{"class":719},[29,733,103],{"class":62},[29,735,736],{"class":719},"extra",[29,738,739],{"class":668}," =",[29,741,742],{"class":62}," {}) {\n",[29,744,745,748],{"class":31,"line":78},[29,746,747],{"class":99},"    super",[29,749,750],{"class":62},"(detail);\n",[29,752,753,756,759,762],{"class":31,"line":83},[29,754,755],{"class":99},"    this",[29,757,758],{"class":62},".status ",[29,760,761],{"class":668},"=",[29,763,764],{"class":62}," status;\n",[29,766,767,769,772,774],{"class":31,"line":89},[29,768,755],{"class":99},[29,770,771],{"class":62},".code ",[29,773,761],{"class":668},[29,775,776],{"class":62}," code;\n",[29,778,779,781,784,786],{"class":31,"line":126},[29,780,755],{"class":99},[29,782,783],{"class":62},".title ",[29,785,761],{"class":668},[29,787,788],{"class":62}," title;\n",[29,790,791,793,796,798],{"class":31,"line":131},[29,792,755],{"class":99},[29,794,795],{"class":62},".extra ",[29,797,761],{"class":668},[29,799,800],{"class":62}," extra;\n",[29,802,803],{"class":31,"line":137},[29,804,805],{"class":62},"  }\n",[29,807,808],{"class":31,"line":626},[29,809,629],{"class":62},[29,811,813],{"class":31,"line":812},13,[29,814,50],{"emptyLinePlaceholder":49},[29,816,818],{"class":31,"line":817},14,[29,819,820],{"class":35},"\u002F\u002F 2. The single place that turns any error into Problem Details\n",[29,822,824,827,830,832,835,837,840,842,845],{"class":31,"line":823},15,[29,825,826],{"class":668},"function",[29,828,829],{"class":698}," sendProblem",[29,831,716],{"class":62},[29,833,834],{"class":719},"req",[29,836,103],{"class":62},[29,838,839],{"class":719},"res",[29,841,103],{"class":62},[29,843,844],{"class":719},"err",[29,846,847],{"class":62},") {\n",[29,849,851,854,857,859,862],{"class":31,"line":850},16,[29,852,853],{"class":668},"  const",[29,855,856],{"class":99}," traceId",[29,858,739],{"class":668},[29,860,861],{"class":698}," randomUUID",[29,863,864],{"class":62},"();\n",[29,866,868],{"class":31,"line":867},17,[29,869,50],{"emptyLinePlaceholder":49},[29,871,873,876],{"class":31,"line":872},18,[29,874,875],{"class":668},"  let",[29,877,878],{"class":62}," body;\n",[29,880,882,885,888,891,893],{"class":31,"line":881},19,[29,883,884],{"class":668},"  if",[29,886,887],{"class":62}," (err ",[29,889,890],{"class":668},"instanceof",[29,892,699],{"class":698},[29,894,847],{"class":62},[29,896,898,901,903],{"class":31,"line":897},20,[29,899,900],{"class":62},"    body ",[29,902,761],{"class":668},[29,904,708],{"class":62},[29,906,908,911,914,916,918,920,923],{"class":31,"line":907},21,[29,909,910],{"class":62},"      type: ",[29,912,913],{"class":42},"`https:\u002F\u002Fapi.example.com\u002Ferrors\u002F${",[29,915,844],{"class":62},[29,917,313],{"class":42},[29,919,26],{"class":62},[29,921,922],{"class":42},"}`",[29,924,484],{"class":62},[29,926,928],{"class":31,"line":927},22,[29,929,930],{"class":62},"      title: err.title,\n",[29,932,934],{"class":31,"line":933},23,[29,935,936],{"class":62},"      status: err.status,\n",[29,938,940],{"class":31,"line":939},24,[29,941,942],{"class":62},"      detail: err.message,\n",[29,944,946],{"class":31,"line":945},25,[29,947,948],{"class":62},"      instance: req.url,\n",[29,950,952],{"class":31,"line":951},26,[29,953,954],{"class":62},"      code: err.code,\n",[29,956,958],{"class":31,"line":957},27,[29,959,960],{"class":62},"      traceId,\n",[29,962,964,967],{"class":31,"line":963},28,[29,965,966],{"class":668},"      ...",[29,968,969],{"class":62},"err.extra,\n",[29,971,973],{"class":31,"line":972},29,[29,974,975],{"class":62},"    };\n",[29,977,979,982,985],{"class":31,"line":978},30,[29,980,981],{"class":62},"  } ",[29,983,984],{"class":668},"else",[29,986,708],{"class":62},[29,988,990],{"class":31,"line":989},31,[29,991,992],{"class":35},"    \u002F\u002F Unexpected error: keep the details in the server log only\n",[29,994,996,999,1002,1004,1007,1009,1012],{"class":31,"line":995},32,[29,997,998],{"class":62},"    console.",[29,1000,1001],{"class":698},"error",[29,1003,716],{"class":62},[29,1005,1006],{"class":42},"`[${",[29,1008,430],{"class":62},[29,1010,1011],{"class":42},"}]`",[29,1013,1014],{"class":62},", err);\n",[29,1016,1018,1020,1022],{"class":31,"line":1017},33,[29,1019,900],{"class":62},[29,1021,761],{"class":668},[29,1023,708],{"class":62},[29,1025,1027,1029,1031],{"class":31,"line":1026},34,[29,1028,910],{"class":62},[29,1030,361],{"class":42},[29,1032,484],{"class":62},[29,1034,1036,1039,1042],{"class":31,"line":1035},35,[29,1037,1038],{"class":62},"      title: ",[29,1040,1041],{"class":42},"\"Internal Server Error\"",[29,1043,484],{"class":62},[29,1045,1047,1050,1053],{"class":31,"line":1046},36,[29,1048,1049],{"class":62},"      status: ",[29,1051,1052],{"class":99},"500",[29,1054,484],{"class":62},[29,1056,1058,1061,1064],{"class":31,"line":1057},37,[29,1059,1060],{"class":62},"      detail: ",[29,1062,1063],{"class":42},"\"Something went wrong while processing the request.\"",[29,1065,484],{"class":62},[29,1067,1069],{"class":31,"line":1068},38,[29,1070,948],{"class":62},[29,1072,1074],{"class":31,"line":1073},39,[29,1075,960],{"class":62},[29,1077,1079],{"class":31,"line":1078},40,[29,1080,975],{"class":62},[29,1082,1084],{"class":31,"line":1083},41,[29,1085,805],{"class":62},[29,1087,1089],{"class":31,"line":1088},42,[29,1090,50],{"emptyLinePlaceholder":49},[29,1092,1094,1097,1099],{"class":31,"line":1093},43,[29,1095,1096],{"class":62},"  res.statusCode ",[29,1098,761],{"class":668},[29,1100,1101],{"class":62}," body.status;\n",[29,1103,1105,1108,1111,1113,1116,1118,1121],{"class":31,"line":1104},44,[29,1106,1107],{"class":62},"  res.",[29,1109,1110],{"class":698},"setHeader",[29,1112,716],{"class":62},[29,1114,1115],{"class":42},"\"Content-Type\"",[29,1117,103],{"class":62},[29,1119,1120],{"class":42},"\"application\u002Fproblem+json\"",[29,1122,1123],{"class":62},");\n",[29,1125,1127,1129,1132,1134,1137,1139,1142],{"class":31,"line":1126},45,[29,1128,1107],{"class":62},[29,1130,1131],{"class":698},"end",[29,1133,716],{"class":62},[29,1135,1136],{"class":99},"JSON",[29,1138,313],{"class":62},[29,1140,1141],{"class":698},"stringify",[29,1143,1144],{"class":62},"(body));\n",[29,1146,1148],{"class":31,"line":1147},46,[29,1149,629],{"class":62},[15,1151,1152,1153,313],{},"Business logic just throws the right ",[26,1154,1155],{},"ApiError",[19,1157,1159],{"className":21,"code":1158,"language":23,"meta":24,"style":24},"async function createUser(input) {\n  const errors = [];\n  if (!isEmail(input.email)) {\n    errors.push({ field: \"email\", code: \"invalid_format\", message: \"Email format is invalid.\" });\n  }\n  if ((input.password ?? \"\").length \u003C 8) {\n    errors.push({ field: \"password\", code: \"too_short\", message: \"Password must be at least 8 characters.\" });\n  }\n  if (errors.length > 0) {\n    throw new ApiError(422, \"validation_failed\", \"Invalid input\",\n      `Please check ${errors.length} fields.`, { errors });\n  }\n\n  if (await emailExists(input.email)) {\n    throw new ApiError(409, \"email_taken\", \"Email already in use\",\n      `${input.email} is already registered.`);\n  }\n  \u002F\u002F ...\n}\n",[26,1160,1161,1179,1191,1206,1232,1236,1263,1283,1287,1304,1328,1345,1349,1353,1367,1392,1409,1413,1418],{"__ignoreMap":24},[29,1162,1163,1166,1169,1172,1174,1177],{"class":31,"line":32},[29,1164,1165],{"class":668},"async",[29,1167,1168],{"class":668}," function",[29,1170,1171],{"class":698}," createUser",[29,1173,716],{"class":62},[29,1175,1176],{"class":719},"input",[29,1178,847],{"class":62},[29,1180,1181,1183,1186,1188],{"class":31,"line":39},[29,1182,853],{"class":668},[29,1184,1185],{"class":99}," errors",[29,1187,739],{"class":668},[29,1189,1190],{"class":62}," [];\n",[29,1192,1193,1195,1197,1200,1203],{"class":31,"line":46},[29,1194,884],{"class":668},[29,1196,638],{"class":62},[29,1198,1199],{"class":668},"!",[29,1201,1202],{"class":698},"isEmail",[29,1204,1205],{"class":62},"(input.email)) {\n",[29,1207,1208,1211,1214,1217,1219,1222,1224,1227,1229],{"class":31,"line":53},[29,1209,1210],{"class":62},"    errors.",[29,1212,1213],{"class":698},"push",[29,1215,1216],{"class":62},"({ field: ",[29,1218,565],{"class":42},[29,1220,1221],{"class":62},", code: ",[29,1223,575],{"class":42},[29,1225,1226],{"class":62},", message: ",[29,1228,584],{"class":42},[29,1230,1231],{"class":62}," });\n",[29,1233,1234],{"class":31,"line":59},[29,1235,805],{"class":62},[29,1237,1238,1240,1243,1246,1249,1252,1255,1258,1261],{"class":31,"line":78},[29,1239,884],{"class":668},[29,1241,1242],{"class":62}," ((input.password ",[29,1244,1245],{"class":668},"??",[29,1247,1248],{"class":42}," \"\"",[29,1250,1251],{"class":62},").",[29,1253,1254],{"class":99},"length",[29,1256,1257],{"class":668}," \u003C",[29,1259,1260],{"class":99}," 8",[29,1262,847],{"class":62},[29,1264,1265,1267,1269,1271,1273,1275,1277,1279,1281],{"class":31,"line":83},[29,1266,1210],{"class":62},[29,1268,1213],{"class":698},[29,1270,1216],{"class":62},[29,1272,598],{"class":42},[29,1274,1221],{"class":62},[29,1276,607],{"class":42},[29,1278,1226],{"class":62},[29,1280,616],{"class":42},[29,1282,1231],{"class":62},[29,1284,1285],{"class":31,"line":89},[29,1286,805],{"class":62},[29,1288,1289,1291,1294,1296,1299,1302],{"class":31,"line":126},[29,1290,884],{"class":668},[29,1292,1293],{"class":62}," (errors.",[29,1295,1254],{"class":99},[29,1297,1298],{"class":668}," >",[29,1300,1301],{"class":99}," 0",[29,1303,847],{"class":62},[29,1305,1306,1309,1312,1314,1316,1318,1320,1322,1324,1326],{"class":31,"line":131},[29,1307,1308],{"class":668},"    throw",[29,1310,1311],{"class":668}," new",[29,1313,699],{"class":698},[29,1315,716],{"class":62},[29,1317,506],{"class":99},[29,1319,103],{"class":62},[29,1321,542],{"class":42},[29,1323,103],{"class":62},[29,1325,494],{"class":42},[29,1327,484],{"class":62},[29,1329,1330,1333,1335,1337,1339,1342],{"class":31,"line":137},[29,1331,1332],{"class":42},"      `Please check ${",[29,1334,427],{"class":62},[29,1336,313],{"class":42},[29,1338,1254],{"class":99},[29,1340,1341],{"class":42},"} fields.`",[29,1343,1344],{"class":62},", { errors });\n",[29,1346,1347],{"class":31,"line":626},[29,1348,805],{"class":62},[29,1350,1351],{"class":31,"line":812},[29,1352,50],{"emptyLinePlaceholder":49},[29,1354,1355,1357,1359,1362,1365],{"class":31,"line":817},[29,1356,884],{"class":668},[29,1358,638],{"class":62},[29,1360,1361],{"class":668},"await",[29,1363,1364],{"class":698}," emailExists",[29,1366,1205],{"class":62},[29,1368,1369,1371,1373,1375,1377,1380,1382,1385,1387,1390],{"class":31,"line":823},[29,1370,1308],{"class":668},[29,1372,1311],{"class":668},[29,1374,699],{"class":698},[29,1376,716],{"class":62},[29,1378,1379],{"class":99},"409",[29,1381,103],{"class":62},[29,1383,1384],{"class":42},"\"email_taken\"",[29,1386,103],{"class":62},[29,1388,1389],{"class":42},"\"Email already in use\"",[29,1391,484],{"class":62},[29,1393,1394,1397,1399,1401,1404,1407],{"class":31,"line":850},[29,1395,1396],{"class":42},"      `${",[29,1398,1176],{"class":62},[29,1400,313],{"class":42},[29,1402,1403],{"class":62},"email",[29,1405,1406],{"class":42},"} is already registered.`",[29,1408,1123],{"class":62},[29,1410,1411],{"class":31,"line":867},[29,1412,805],{"class":62},[29,1414,1415],{"class":31,"line":872},[29,1416,1417],{"class":35},"  \u002F\u002F ...\n",[29,1419,1420],{"class":31,"line":881},[29,1421,629],{"class":62},[15,1423,1424],{},"Sending real requests through this setup gives:",[193,1426,1427,1437],{},[196,1428,1429],{},[199,1430,1431,1434],{},[202,1432,1433],{},"Request",[202,1435,1436],{},"Response",[212,1438,1439,1458,1470,1483,1496],{},[199,1440,1441,1451],{},[217,1442,1443,1444,1447,1448],{},"Email ",[26,1445,1446],{},"kim",", password ",[26,1449,1450],{},"123",[217,1452,1453,1455,1456],{},[26,1454,506],{},", both fields in ",[26,1457,427],{},[199,1459,1460,1463],{},[217,1461,1462],{},"An already-registered email",[217,1464,1465,103,1467],{},[26,1466,1379],{},[26,1468,1469],{},"code: \"email_taken\"",[199,1471,1472,1475],{},[217,1473,1474],{},"Broken JSON",[217,1476,1477,103,1480],{},[26,1478,1479],{},"400",[26,1481,1482],{},"code: \"invalid_json\"",[199,1484,1485,1488],{},[217,1486,1487],{},"An unexpected error like a failed DB connection",[217,1489,1490,1492,1493,1495],{},[26,1491,1052],{},", a generic message + ",[26,1494,430],{}," (the DB address only in the server log)",[199,1497,1498,1501],{},[217,1499,1500],{},"A valid request",[217,1502,1503,1506,1507],{},[26,1504,1505],{},"201"," + ",[26,1508,1509],{},"Location",[15,1511,1512,1513,1516,1517,1520],{},"With Express, register ",[26,1514,1515],{},"sendProblem"," as error-handling middleware (",[26,1518,1519],{},"(err, req, res, next) => ...",") and you get the same structure.",[186,1522],{},[10,1524,1526],{"id":1525},"consuming-it-on-the-frontend","Consuming it on the frontend",[15,1528,1529],{},"Since every error has the same shape, one piece of handling code is enough.",[19,1531,1533],{"className":21,"code":1532,"language":23,"meta":24,"style":24},"async function api(url, options) {\n  const res = await fetch(url, options);\n  if (res.ok) return res.status === 204 ? null : res.json();\n\n  const problem = await res.json().catch(() => ({ status: res.status, title: res.statusText }));\n  const error = new Error(problem.detail ?? problem.title);\n  error.problem = problem;\n  throw error;\n}\n\ntry {\n  await api(\"\u002Fusers\", { method: \"POST\", body: JSON.stringify(form) });\n} catch (e) {\n  const p = e.problem;\n  if (p?.code === \"validation_failed\") {\n    p.errors.forEach(({ field, message }) => showFieldError(field, message));\n  } else if (p?.code === \"email_taken\") {\n    showFieldError(\"email\", p.detail);\n  } else {\n    toast(`Something went wrong. (Reference: ${p?.traceId ?? \"none\"})`);\n  }\n}\n",[26,1534,1535,1556,1574,1609,1613,1643,1664,1674,1682,1686,1690,1697,1726,1736,1748,1762,1791,1809,1821,1829,1854,1858],{"__ignoreMap":24},[29,1536,1537,1539,1541,1544,1546,1549,1551,1554],{"class":31,"line":32},[29,1538,1165],{"class":668},[29,1540,1168],{"class":668},[29,1542,1543],{"class":698}," api",[29,1545,716],{"class":62},[29,1547,1548],{"class":719},"url",[29,1550,103],{"class":62},[29,1552,1553],{"class":719},"options",[29,1555,847],{"class":62},[29,1557,1558,1560,1563,1565,1568,1571],{"class":31,"line":39},[29,1559,853],{"class":668},[29,1561,1562],{"class":99}," res",[29,1564,739],{"class":668},[29,1566,1567],{"class":668}," await",[29,1569,1570],{"class":698}," fetch",[29,1572,1573],{"class":62},"(url, options);\n",[29,1575,1576,1578,1581,1584,1587,1590,1593,1596,1599,1602,1605,1607],{"class":31,"line":46},[29,1577,884],{"class":668},[29,1579,1580],{"class":62}," (res.ok) ",[29,1582,1583],{"class":668},"return",[29,1585,1586],{"class":62}," res.status ",[29,1588,1589],{"class":668},"===",[29,1591,1592],{"class":99}," 204",[29,1594,1595],{"class":668}," ?",[29,1597,1598],{"class":99}," null",[29,1600,1601],{"class":668}," :",[29,1603,1604],{"class":62}," res.",[29,1606,464],{"class":698},[29,1608,864],{"class":62},[29,1610,1611],{"class":31,"line":53},[29,1612,50],{"emptyLinePlaceholder":49},[29,1614,1615,1617,1620,1622,1624,1626,1628,1631,1634,1637,1640],{"class":31,"line":59},[29,1616,853],{"class":668},[29,1618,1619],{"class":99}," problem",[29,1621,739],{"class":668},[29,1623,1567],{"class":668},[29,1625,1604],{"class":62},[29,1627,464],{"class":698},[29,1629,1630],{"class":62},"().",[29,1632,1633],{"class":698},"catch",[29,1635,1636],{"class":62},"(() ",[29,1638,1639],{"class":668},"=>",[29,1641,1642],{"class":62}," ({ status: res.status, title: res.statusText }));\n",[29,1644,1645,1647,1650,1652,1654,1656,1659,1661],{"class":31,"line":78},[29,1646,853],{"class":668},[29,1648,1649],{"class":99}," error",[29,1651,739],{"class":668},[29,1653,1311],{"class":668},[29,1655,705],{"class":698},[29,1657,1658],{"class":62},"(problem.detail ",[29,1660,1245],{"class":668},[29,1662,1663],{"class":62}," problem.title);\n",[29,1665,1666,1669,1671],{"class":31,"line":83},[29,1667,1668],{"class":62},"  error.problem ",[29,1670,761],{"class":668},[29,1672,1673],{"class":62}," problem;\n",[29,1675,1676,1679],{"class":31,"line":89},[29,1677,1678],{"class":668},"  throw",[29,1680,1681],{"class":62}," error;\n",[29,1683,1684],{"class":31,"line":126},[29,1685,629],{"class":62},[29,1687,1688],{"class":31,"line":131},[29,1689,50],{"emptyLinePlaceholder":49},[29,1691,1692,1695],{"class":31,"line":137},[29,1693,1694],{"class":668},"try",[29,1696,708],{"class":62},[29,1698,1699,1702,1704,1706,1708,1711,1714,1717,1719,1721,1723],{"class":31,"line":626},[29,1700,1701],{"class":668},"  await",[29,1703,1543],{"class":698},[29,1705,716],{"class":62},[29,1707,530],{"class":42},[29,1709,1710],{"class":62},", { method: ",[29,1712,1713],{"class":42},"\"POST\"",[29,1715,1716],{"class":62},", body: ",[29,1718,1136],{"class":99},[29,1720,313],{"class":62},[29,1722,1141],{"class":698},[29,1724,1725],{"class":62},"(form) });\n",[29,1727,1728,1731,1733],{"class":31,"line":812},[29,1729,1730],{"class":62},"} ",[29,1732,1633],{"class":668},[29,1734,1735],{"class":62}," (e) {\n",[29,1737,1738,1740,1743,1745],{"class":31,"line":817},[29,1739,853],{"class":668},[29,1741,1742],{"class":99}," p",[29,1744,739],{"class":668},[29,1746,1747],{"class":62}," e.problem;\n",[29,1749,1750,1752,1755,1757,1760],{"class":31,"line":823},[29,1751,884],{"class":668},[29,1753,1754],{"class":62}," (p?.code ",[29,1756,1589],{"class":668},[29,1758,1759],{"class":42}," \"validation_failed\"",[29,1761,847],{"class":62},[29,1763,1764,1767,1770,1773,1776,1778,1780,1783,1785,1788],{"class":31,"line":850},[29,1765,1766],{"class":62},"    p.errors.",[29,1768,1769],{"class":698},"forEach",[29,1771,1772],{"class":62},"(({ ",[29,1774,1775],{"class":719},"field",[29,1777,103],{"class":62},[29,1779,645],{"class":719},[29,1781,1782],{"class":62}," }) ",[29,1784,1639],{"class":668},[29,1786,1787],{"class":698}," showFieldError",[29,1789,1790],{"class":62},"(field, message));\n",[29,1792,1793,1795,1797,1800,1802,1804,1807],{"class":31,"line":867},[29,1794,981],{"class":62},[29,1796,984],{"class":668},[29,1798,1799],{"class":668}," if",[29,1801,1754],{"class":62},[29,1803,1589],{"class":668},[29,1805,1806],{"class":42}," \"email_taken\"",[29,1808,847],{"class":62},[29,1810,1811,1814,1816,1818],{"class":31,"line":872},[29,1812,1813],{"class":698},"    showFieldError",[29,1815,716],{"class":62},[29,1817,565],{"class":42},[29,1819,1820],{"class":62},", p.detail);\n",[29,1822,1823,1825,1827],{"class":31,"line":881},[29,1824,981],{"class":62},[29,1826,984],{"class":668},[29,1828,708],{"class":62},[29,1830,1831,1834,1836,1839,1841,1844,1846,1849,1852],{"class":31,"line":897},[29,1832,1833],{"class":698},"    toast",[29,1835,716],{"class":62},[29,1837,1838],{"class":42},"`Something went wrong. (Reference: ${",[29,1840,15],{"class":62},[29,1842,1843],{"class":42},"?.",[29,1845,430],{"class":62},[29,1847,1848],{"class":668}," ??",[29,1850,1851],{"class":42}," \"none\"})`",[29,1853,1123],{"class":62},[29,1855,1856],{"class":31,"line":907},[29,1857,805],{"class":62},[29,1859,1860],{"class":31,"line":927},[29,1861,629],{"class":62},[15,1863,1864,1865,1867],{},"When a user gives support the ",[26,1866,430],{},", you can find the actual error for that request in the server logs right away.",[186,1869],{},[10,1871,1873],{"id":1872},"summary-why-this-is-worth-knowing","Summary: why this is worth knowing",[193,1875,1876,1886],{},[196,1877,1878],{},[199,1879,1880,1883],{},[202,1881,1882],{},"Principle",[202,1884,1885],{},"How",[212,1887,1888,1899,1910,1923,1934,1945,1953],{},[199,1889,1890,1893],{},[217,1891,1892],{},"Same shape across every API",[217,1894,1895,1896,1898],{},"RFC 9457 Problem Details (",[26,1897,413],{},")",[199,1900,1901,1904],{},[217,1902,1903],{},"Something programs can branch on",[217,1905,1906,1907,1909],{},"A stable ",[26,1908,26],{}," (never branch on message strings)",[199,1911,1912,1915],{},[217,1913,1914],{},"An explanation for people",[217,1916,1917,1919,1920,1922],{},[26,1918,369],{}," (the kind), ",[26,1921,391],{}," (this occurrence)",[199,1924,1925,1928],{},[217,1926,1927],{},"Form validation failures",[217,1929,1930,1931,1933],{},"An ",[26,1932,427],{}," array, all fields at once",[199,1935,1936,1939],{},[217,1937,1938],{},"Support requests",[217,1940,1941,1942,1944],{},"A ",[26,1943,430],{}," linking to server logs",[199,1946,1947,1950],{},[217,1948,1949],{},"Security",[217,1951,1952],{},"Generic messages for unexpected errors, details only in logs",[199,1954,1955,1958],{},[217,1956,1957],{},"Staying consistent",[217,1959,1960],{},"One error class + one error response function",[15,1962,1963],{},"If status codes say \"what kind of failure,\" the error body says \"so what should I do about it.\" Settle the format once, and as your API grows, the frontend's error handling code doesn't.",[1965,1966,1967],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":24,"searchDepth":39,"depth":39,"links":1969},[1970,1971,1972,1973,1974,1975,1976],{"id":12,"depth":39,"text":13},{"id":190,"depth":39,"text":191},{"id":318,"depth":39,"text":319},{"id":452,"depth":39,"text":453},{"id":650,"depth":39,"text":651},{"id":1525,"depth":39,"text":1526},{"id":1872,"depth":39,"text":1873},"If one API returns a string, another returns { error }, and a third returns { message, code }, the frontend needs different code for every error. What makes a good error response, the standard RFC 9457 Problem Details format, per-field validation errors, hiding internals, and handling it all in one place in Node.js.","md",{"date":1980,"field":1981,"tags":1982},"2026.10.08","backend",[1983,1984],"api","http","\u002Fen\u002Fapi-error-response-format",{"title":5,"description":1977},{"loc":1985},"en\u002Fapi-error-response-format","-N6HuCM_uWcSMMiRHzdx7kO256JQmxm-ZMt1A5KAL5o",1791482015983]