[{"data":1,"prerenderedAt":3616},["ShallowReactive",2],{"Categories":3,"NavIndexCategoriesCountFooter":206,"content-\u002F2026\u002F05\u002F07\u002Ffunctional-error-handling-in-ktor-with-arrow\u002F":207},[4,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,68,70,71,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205],{"category":5},"System Administration",{"category":5},{"category":5},{"category":5},{"category":5},{"category":5},{"category":5},{"category":5},{"category":5},{"category":5},{"category":5},{"category":5},{"category":5},{"category":5},{"category":5},{"category":5},{"category":5},{"category":5},{"category":5},{"category":5},{"category":5},{"category":27},"Software Development",{"category":5},{"category":5},{"category":5},{"category":5},{"category":27},{"category":27},{"category":5},{"category":5},{"category":5},{"category":27},{"category":5},{"category":5},{"category":5},{"category":27},{"category":27},{"category":27},{"category":27},{"category":5},{"category":5},{"category":5},{"category":27},{"category":27},{"category":5},{"category":5},{"category":5},{"category":5},{"category":5},{"category":5},{"category":27},{"category":5},{"category":5},{"category":27},{"category":27},{"category":27},{"category":27},{"category":5},{"category":27},{"category":27},{"category":67},"Drones & RC",{"category":69},"DIY Projects",{"category":67},{"category":72},"Photography",{"category":69},{"category":69},{"category":69},{"category":67},{"category":69},{"category":69},{"category":69},{"category":69},{"category":69},{"category":69},{"category":69},{"category":69},{"category":69},{"category":69},{"category":69},{"category":69},{"category":69},{"category":69},{"category":67},{"category":69},{"category":69},{"category":67},{"category":67},{"category":72},{"category":72},{"category":72},{"category":67},{"category":67},{"category":67},{"category":67},{"category":67},{"category":67},{"category":67},{"category":67},{"category":67},{"category":67},{"category":5},{"category":5},{"category":72},{"category":67},{"category":67},{"category":67},{"category":67},{"category":67},{"category":67},{"category":5},{"category":67},{"category":67},{"category":72},{"category":72},{"category":67},{"category":67},{"category":67},{"category":67},{"category":67},{"category":67},{"category":67},{"category":67},{"category":67},{"category":67},{"category":67},{"category":67},{"category":72},{"category":67},{"category":138},"3D Printing - Laser Cutting - CNC",{"category":138},{"category":138},{"category":138},{"category":138},{"category":138},{"category":138},{"category":138},{"category":138},{"category":138},{"category":138},{"category":138},{"category":5},{"category":138},{"category":27},{"category":27},{"category":138},{"category":138},{"category":72},{"category":158},"Photography,3D Printing - Laser Cutting - CNC",{"category":27},{"category":27},{"category":69},{"category":27},{"category":27},{"category":27},{"category":27},{"category":5},{"category":67},{"category":5},{"category":5},{"category":27},{"category":27},{"category":27},{"category":27},{"category":27},{"category":69},{"category":27},{"category":27},{"category":27},{"category":27},{"category":181},"Home Assistant",{"category":181},{"category":72},{"category":27},{"category":27},{"category":72},{"category":138},{"category":5},{"category":72},{"category":72},{"category":138},{"category":27},{"category":181},{"category":5},{"category":181},{"category":72},{"category":72},{"category":72},{"category":72},{"category":72},{"category":72},{"category":72},{"category":72},{"category":72},{"category":27},194,{"id":208,"title":209,"body":210,"category":27,"date":3602,"description":3603,"embedImage":3604,"extension":3605,"image":3604,"intro":3606,"meta":3607,"navigation":278,"path":3608,"seo":3609,"series":3604,"sitemap":3610,"stem":3611,"tags":3612,"__hash__":3615},"content\u002F2026\u002F05\u002F07\u002Ffunctional-error-handling-in-ktor-with-arrow.md","Functional Error Handling in Ktor with Arrow",{"type":211,"value":212,"toc":3587},"minimark",[213,234,239,242,549,562,568,853,858,861,1041,1045,1048,1391,1401,1405,1412,1765,1801,1822,1826,1848,1851,2205,2219,2223,2228,2325,2339,2343,2354,2362,2483,2493,2608,2620,2624,2634,2883,2909,3054,3079,3082,3184,3190,3194,3205,3366,3376,3417,3420,3458,3462,3469,3518,3524,3528,3542,3583],[214,215,216,217,227,228,233],"p",{},"I use ",[218,219,226],"a",{"href":220,"rel":221,"target":225},"https:\u002F\u002Fktor.io\u002F",[222,223,224],"nofollow","noopener","noreferer","_blank","Ktor"," for a lot of projects - it's lightweight and simple to use. However - when providing any\nsort of API I want to make sure that the errors are actually useful and as specific as possible. For that I like to\nuse ",[218,229,232],{"href":230,"rel":231,"target":225},"https:\u002F\u002Farrow-kt.io\u002F",[222,223,224],"Arrow",".",[235,236,238],"h2",{"id":237},"the-error-model","The Error Model",[214,240,241],{},"Everything starts with a sealed interface that represents every error the API can produce:",[243,244,249],"pre",{"className":245,"code":246,"language":247,"meta":248,"style":248},"language-kotlin shiki shiki-themes github-dark","import io.ktor.http.HttpStatusCode\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class ErrorResponse(\n    @Serializable(with = HttpStatusCodeSerializer::class)\n    val status: HttpStatusCode,\n    val message: String,\n    val fieldValue: String? = null,\n)\n\nsealed interface ApiError {\n    val response: ErrorResponse\n}\n\nfun ApiError.status() = response.status\n\nfun ApiError.messageMap(): Map\u003CString, ErrorResponse> =\n    when (this) {\n        is UpstreamError -> mapOf(\"upstream\" to upstream, \"error\" to response)\n        else -> mapOf(\"error\" to response)\n    }\n","kotlin","",[250,251,252,265,273,280,286,302,323,338,351,372,377,382,397,408,414,419,440,445,480,495,526,543],"code",{"__ignoreMap":248},[253,254,257,261],"span",{"class":255,"line":256},"line",1,[253,258,260],{"class":259},"snl16","import",[253,262,264],{"class":263},"svObZ"," io.ktor.http.HttpStatusCode\n",[253,266,268,270],{"class":255,"line":267},2,[253,269,260],{"class":259},[253,271,272],{"class":263}," kotlinx.serialization.Serializable\n",[253,274,276],{"class":255,"line":275},3,[253,277,279],{"emptyLinePlaceholder":278},true,"\n",[253,281,283],{"class":255,"line":282},4,[253,284,285],{"class":263},"@Serializable\n",[253,287,289,292,295,298],{"class":255,"line":288},5,[253,290,291],{"class":259},"data",[253,293,294],{"class":259}," class",[253,296,297],{"class":263}," ErrorResponse",[253,299,301],{"class":300},"s95oV","(\n",[253,303,305,308,311,314,317,320],{"class":255,"line":304},6,[253,306,307],{"class":263},"    @Serializable",[253,309,310],{"class":300},"(with ",[253,312,313],{"class":259},"=",[253,315,316],{"class":300}," HttpStatusCodeSerializer::",[253,318,319],{"class":263},"class",[253,321,322],{"class":300},")\n",[253,324,326,329,332,335],{"class":255,"line":325},7,[253,327,328],{"class":259},"    val",[253,330,331],{"class":300}," status: ",[253,333,334],{"class":263},"HttpStatusCode",[253,336,337],{"class":300},",\n",[253,339,341,343,346,349],{"class":255,"line":340},8,[253,342,328],{"class":259},[253,344,345],{"class":300}," message: ",[253,347,348],{"class":263},"String",[253,350,337],{"class":300},[253,352,354,356,359,361,364,366,370],{"class":255,"line":353},9,[253,355,328],{"class":259},[253,357,358],{"class":300}," fieldValue: ",[253,360,348],{"class":263},[253,362,363],{"class":300},"? ",[253,365,313],{"class":259},[253,367,369],{"class":368},"sDLfK"," null",[253,371,337],{"class":300},[253,373,375],{"class":255,"line":374},10,[253,376,322],{"class":300},[253,378,380],{"class":255,"line":379},11,[253,381,279],{"emptyLinePlaceholder":278},[253,383,385,388,391,394],{"class":255,"line":384},12,[253,386,387],{"class":259},"sealed",[253,389,390],{"class":259}," interface",[253,392,393],{"class":263}," ApiError",[253,395,396],{"class":300}," {\n",[253,398,400,402,405],{"class":255,"line":399},13,[253,401,328],{"class":259},[253,403,404],{"class":300}," response: ",[253,406,407],{"class":263},"ErrorResponse\n",[253,409,411],{"class":255,"line":410},14,[253,412,413],{"class":300},"}\n",[253,415,417],{"class":255,"line":416},15,[253,418,279],{"emptyLinePlaceholder":278},[253,420,422,425,427,429,432,435,437],{"class":255,"line":421},16,[253,423,424],{"class":259},"fun",[253,426,393],{"class":263},[253,428,233],{"class":300},[253,430,431],{"class":263},"status",[253,433,434],{"class":300},"() ",[253,436,313],{"class":259},[253,438,439],{"class":300}," response.status\n",[253,441,443],{"class":255,"line":442},17,[253,444,279],{"emptyLinePlaceholder":278},[253,446,448,450,452,454,457,460,463,466,468,471,474,477],{"class":255,"line":447},18,[253,449,424],{"class":259},[253,451,393],{"class":263},[253,453,233],{"class":300},[253,455,456],{"class":263},"messageMap",[253,458,459],{"class":300},"(): ",[253,461,462],{"class":263},"Map",[253,464,465],{"class":300},"\u003C",[253,467,348],{"class":263},[253,469,470],{"class":300},", ",[253,472,473],{"class":263},"ErrorResponse",[253,475,476],{"class":300},"> ",[253,478,479],{"class":259},"=\n",[253,481,483,486,489,492],{"class":255,"line":482},19,[253,484,485],{"class":259},"    when",[253,487,488],{"class":300}," (",[253,490,491],{"class":368},"this",[253,493,494],{"class":300},") {\n",[253,496,498,501,504,507,510,513,517,520,523],{"class":255,"line":497},20,[253,499,500],{"class":259},"        is",[253,502,503],{"class":300}," UpstreamError ",[253,505,506],{"class":259},"->",[253,508,509],{"class":263}," mapOf",[253,511,512],{"class":300},"(",[253,514,516],{"class":515},"sU2Wk","\"upstream\"",[253,518,519],{"class":300}," to upstream, ",[253,521,522],{"class":515},"\"error\"",[253,524,525],{"class":300}," to response)\n",[253,527,529,532,535,537,539,541],{"class":255,"line":528},21,[253,530,531],{"class":259},"        else",[253,533,534],{"class":259}," ->",[253,536,509],{"class":263},[253,538,512],{"class":300},[253,540,522],{"class":515},[253,542,525],{"class":300},[253,544,546],{"class":255,"line":545},22,[253,547,548],{"class":300},"    }\n",[214,550,551,553,554,557,558,561],{},[250,552,473],{}," is what gets serialized to JSON. ",[250,555,556],{},"ApiError"," is the internal type that routes and services work with — it\nnever escapes the process boundary. ",[250,559,560],{},"messageMap()"," shapes the JSON body; upstream errors include both the downstream\nresponse and the error we're reporting to our caller.",[214,563,564,565,567],{},"Because ",[250,566,334],{}," isn't serializable by default, a custom serializer handles it:",[243,569,571],{"className":245,"code":570,"language":247,"meta":248,"style":248},"import io.ktor.http.HttpStatusCode\nimport kotlinx.serialization.KSerializer\nimport kotlinx.serialization.descriptors.buildClassSerialDescriptor\nimport kotlinx.serialization.descriptors.element\nimport kotlinx.serialization.encoding.*\n\nobject HttpStatusCodeSerializer : KSerializer\u003CHttpStatusCode> {\n    override val descriptor =\n        buildClassSerialDescriptor(\"HttpStatusCode\") {\n            element\u003CInt>(\"value\")\n            element\u003CString>(\"description\")\n        }\n\n    override fun deserialize(decoder: Decoder): HttpStatusCode =\n        decoder.decodeStructure(descriptor) {\n            HttpStatusCode.fromValue(decodeIntElement(descriptor, 0))\n        }\n\n    override fun serialize(encoder: Encoder, value: HttpStatusCode) =\n        encoder.encodeStructure(descriptor) {\n            encodeIntElement(descriptor, 0, value.value)\n            encodeStringElement(descriptor, 1, value.description)\n        }\n}\n",[250,572,573,579,586,593,600,610,614,635,648,660,678,693,698,702,726,737,759,763,767,797,807,826,843,848],{"__ignoreMap":248},[253,574,575,577],{"class":255,"line":256},[253,576,260],{"class":259},[253,578,264],{"class":263},[253,580,581,583],{"class":255,"line":267},[253,582,260],{"class":259},[253,584,585],{"class":263}," kotlinx.serialization.KSerializer\n",[253,587,588,590],{"class":255,"line":275},[253,589,260],{"class":259},[253,591,592],{"class":263}," kotlinx.serialization.descriptors.buildClassSerialDescriptor\n",[253,594,595,597],{"class":255,"line":282},[253,596,260],{"class":259},[253,598,599],{"class":263}," kotlinx.serialization.descriptors.element\n",[253,601,602,604,607],{"class":255,"line":288},[253,603,260],{"class":259},[253,605,606],{"class":263}," kotlinx.serialization.encoding.",[253,608,609],{"class":368},"*\n",[253,611,612],{"class":255,"line":304},[253,613,279],{"emptyLinePlaceholder":278},[253,615,616,619,622,625,628,630,632],{"class":255,"line":325},[253,617,618],{"class":259},"object",[253,620,621],{"class":263}," HttpStatusCodeSerializer",[253,623,624],{"class":300}," : ",[253,626,627],{"class":263},"KSerializer",[253,629,465],{"class":300},[253,631,334],{"class":263},[253,633,634],{"class":300},"> {\n",[253,636,637,640,643,646],{"class":255,"line":340},[253,638,639],{"class":259},"    override",[253,641,642],{"class":259}," val",[253,644,645],{"class":300}," descriptor ",[253,647,479],{"class":259},[253,649,650,653,655,658],{"class":255,"line":353},[253,651,652],{"class":263},"        buildClassSerialDescriptor",[253,654,512],{"class":300},[253,656,657],{"class":515},"\"HttpStatusCode\"",[253,659,494],{"class":300},[253,661,662,665,667,670,673,676],{"class":255,"line":374},[253,663,664],{"class":263},"            element",[253,666,465],{"class":300},[253,668,669],{"class":263},"Int",[253,671,672],{"class":300},">(",[253,674,675],{"class":515},"\"value\"",[253,677,322],{"class":300},[253,679,680,682,684,686,688,691],{"class":255,"line":379},[253,681,664],{"class":263},[253,683,465],{"class":300},[253,685,348],{"class":263},[253,687,672],{"class":300},[253,689,690],{"class":515},"\"description\"",[253,692,322],{"class":300},[253,694,695],{"class":255,"line":384},[253,696,697],{"class":300},"        }\n",[253,699,700],{"class":255,"line":399},[253,701,279],{"emptyLinePlaceholder":278},[253,703,704,706,709,712,715,718,721,723],{"class":255,"line":410},[253,705,639],{"class":259},[253,707,708],{"class":259}," fun",[253,710,711],{"class":263}," deserialize",[253,713,714],{"class":300},"(decoder: ",[253,716,717],{"class":263},"Decoder",[253,719,720],{"class":300},"): ",[253,722,334],{"class":263},[253,724,725],{"class":259}," =\n",[253,727,728,731,734],{"class":255,"line":416},[253,729,730],{"class":300},"        decoder.",[253,732,733],{"class":263},"decodeStructure",[253,735,736],{"class":300},"(descriptor) {\n",[253,738,739,742,745,747,750,753,756],{"class":255,"line":421},[253,740,741],{"class":300},"            HttpStatusCode.",[253,743,744],{"class":263},"fromValue",[253,746,512],{"class":300},[253,748,749],{"class":263},"decodeIntElement",[253,751,752],{"class":300},"(descriptor, ",[253,754,755],{"class":368},"0",[253,757,758],{"class":300},"))\n",[253,760,761],{"class":255,"line":442},[253,762,697],{"class":300},[253,764,765],{"class":255,"line":447},[253,766,279],{"emptyLinePlaceholder":278},[253,768,769,771,773,776,779,782,784,787,790,792,795],{"class":255,"line":482},[253,770,639],{"class":259},[253,772,708],{"class":259},[253,774,775],{"class":263}," serialize",[253,777,778],{"class":300},"(encoder: ",[253,780,781],{"class":263},"Encoder",[253,783,470],{"class":300},[253,785,786],{"class":259},"value",[253,788,789],{"class":300},": ",[253,791,334],{"class":263},[253,793,794],{"class":300},") ",[253,796,479],{"class":259},[253,798,799,802,805],{"class":255,"line":497},[253,800,801],{"class":300},"        encoder.",[253,803,804],{"class":263},"encodeStructure",[253,806,736],{"class":300},[253,808,809,812,814,816,818,820,822,824],{"class":255,"line":528},[253,810,811],{"class":263},"            encodeIntElement",[253,813,752],{"class":300},[253,815,755],{"class":368},[253,817,470],{"class":300},[253,819,786],{"class":259},[253,821,233],{"class":300},[253,823,786],{"class":259},[253,825,322],{"class":300},[253,827,828,831,833,836,838,840],{"class":255,"line":545},[253,829,830],{"class":263},"            encodeStringElement",[253,832,752],{"class":300},[253,834,835],{"class":368},"1",[253,837,470],{"class":300},[253,839,786],{"class":259},[253,841,842],{"class":300},".description)\n",[253,844,846],{"class":255,"line":845},23,[253,847,697],{"class":300},[253,849,851],{"class":255,"line":850},24,[253,852,413],{"class":300},[854,855,857],"h3",{"id":856},"abstract-base-classes","Abstract base classes",[214,859,860],{},"Two abstract classes cover the most common error shapes and keep concrete errors concise:",[243,862,864],{"className":245,"code":863,"language":247,"meta":248,"style":248},"abstract class UpstreamError(\n    open val upstream: ErrorResponse,\n    val systemName: String,\n) : ApiError {\n    override val response = ErrorResponse(\n        status = HttpStatusCode.InternalServerError,\n        message = \"call to $systemName failed\",\n    )\n}\n\nabstract class RequiredField(val fieldName: String) : ApiError {\n    override val response = ErrorResponse(\n        status = HttpStatusCode.BadRequest,\n        message = \"$fieldName required\",\n    )\n}\n",[250,865,866,878,892,903,912,927,937,955,960,964,968,993,1007,1016,1033,1037],{"__ignoreMap":248},[253,867,868,871,873,876],{"class":255,"line":256},[253,869,870],{"class":259},"abstract",[253,872,294],{"class":259},[253,874,875],{"class":263}," UpstreamError",[253,877,301],{"class":300},[253,879,880,883,885,888,890],{"class":255,"line":267},[253,881,882],{"class":259},"    open",[253,884,642],{"class":259},[253,886,887],{"class":300}," upstream: ",[253,889,473],{"class":263},[253,891,337],{"class":300},[253,893,894,896,899,901],{"class":255,"line":275},[253,895,328],{"class":259},[253,897,898],{"class":300}," systemName: ",[253,900,348],{"class":263},[253,902,337],{"class":300},[253,904,905,908,910],{"class":255,"line":282},[253,906,907],{"class":300},") : ",[253,909,556],{"class":263},[253,911,396],{"class":300},[253,913,914,916,918,921,923,925],{"class":255,"line":288},[253,915,639],{"class":259},[253,917,642],{"class":259},[253,919,920],{"class":300}," response ",[253,922,313],{"class":259},[253,924,297],{"class":263},[253,926,301],{"class":300},[253,928,929,932,934],{"class":255,"line":304},[253,930,931],{"class":300},"        status ",[253,933,313],{"class":259},[253,935,936],{"class":300}," HttpStatusCode.InternalServerError,\n",[253,938,939,942,944,947,950,953],{"class":255,"line":325},[253,940,941],{"class":300},"        message ",[253,943,313],{"class":259},[253,945,946],{"class":515}," \"call to ",[253,948,949],{"class":368},"$systemName",[253,951,952],{"class":515}," failed\"",[253,954,337],{"class":300},[253,956,957],{"class":255,"line":340},[253,958,959],{"class":300},"    )\n",[253,961,962],{"class":255,"line":353},[253,963,413],{"class":300},[253,965,966],{"class":255,"line":374},[253,967,279],{"emptyLinePlaceholder":278},[253,969,970,972,974,977,979,982,985,987,989,991],{"class":255,"line":379},[253,971,870],{"class":259},[253,973,294],{"class":259},[253,975,976],{"class":263}," RequiredField",[253,978,512],{"class":300},[253,980,981],{"class":259},"val",[253,983,984],{"class":300}," fieldName: ",[253,986,348],{"class":263},[253,988,907],{"class":300},[253,990,556],{"class":263},[253,992,396],{"class":300},[253,994,995,997,999,1001,1003,1005],{"class":255,"line":384},[253,996,639],{"class":259},[253,998,642],{"class":259},[253,1000,920],{"class":300},[253,1002,313],{"class":259},[253,1004,297],{"class":263},[253,1006,301],{"class":300},[253,1008,1009,1011,1013],{"class":255,"line":399},[253,1010,931],{"class":300},[253,1012,313],{"class":259},[253,1014,1015],{"class":300}," HttpStatusCode.BadRequest,\n",[253,1017,1018,1020,1022,1025,1028,1031],{"class":255,"line":410},[253,1019,941],{"class":300},[253,1021,313],{"class":259},[253,1023,1024],{"class":515}," \"",[253,1026,1027],{"class":368},"$fieldName",[253,1029,1030],{"class":515}," required\"",[253,1032,337],{"class":300},[253,1034,1035],{"class":255,"line":416},[253,1036,959],{"class":300},[253,1038,1039],{"class":255,"line":421},[253,1040,413],{"class":300},[854,1042,1044],{"id":1043},"concrete-error-types","Concrete error types",[214,1046,1047],{},"With the base classes in place, project-specific errors are just a few lines each:",[243,1049,1051],{"className":245,"code":1050,"language":247,"meta":248,"style":248},"\u002F\u002F A required field was missing\ndata object TitleRequired : RequiredField(fieldName = \"title\")\n\n\u002F\u002F A resource wasn't found\ndata class NoteNotFound(val id: Int) : ApiError {\n    override val response = ErrorResponse(\n        status = HttpStatusCode.NotFound,\n        message = \"Note not found: $id\",\n    )\n}\n\n\u002F\u002F An ID parameter couldn't be parsed\ndata class IdMalformed(val value: String) : ApiError {\n    override val response = ErrorResponse(\n        status = HttpStatusCode.BadRequest,\n        message = \"ID is not a valid integer: $value\",\n        fieldValue = value,\n    )\n}\n\n\u002F\u002F An upstream service call failed\ndata class StorageCallFailed(override val upstream: ErrorResponse) :\n    UpstreamError(upstream = upstream, systemName = \"Storage Service\")\n\n\u002F\u002F The version file couldn't be read (covered later)\ndata class VersionNotReadable(val e: Throwable) : ApiError {\n    override val response = ErrorResponse(\n        status = HttpStatusCode.InternalServerError,\n        message = \"${e.message}\",\n    )\n}\n",[250,1052,1053,1059,1084,1088,1093,1117,1131,1140,1157,1161,1165,1169,1174,1200,1214,1222,1238,1249,1253,1257,1261,1266,1289,1309,1313,1319,1345,1360,1369,1381,1386],{"__ignoreMap":248},[253,1054,1055],{"class":255,"line":256},[253,1056,1058],{"class":1057},"sAwPA","\u002F\u002F A required field was missing\n",[253,1060,1061,1063,1066,1069,1071,1074,1077,1079,1082],{"class":255,"line":267},[253,1062,291],{"class":259},[253,1064,1065],{"class":259}," object",[253,1067,1068],{"class":263}," TitleRequired",[253,1070,624],{"class":300},[253,1072,1073],{"class":263},"RequiredField",[253,1075,1076],{"class":300},"(fieldName ",[253,1078,313],{"class":259},[253,1080,1081],{"class":515}," \"title\"",[253,1083,322],{"class":300},[253,1085,1086],{"class":255,"line":275},[253,1087,279],{"emptyLinePlaceholder":278},[253,1089,1090],{"class":255,"line":282},[253,1091,1092],{"class":1057},"\u002F\u002F A resource wasn't found\n",[253,1094,1095,1097,1099,1102,1104,1106,1109,1111,1113,1115],{"class":255,"line":288},[253,1096,291],{"class":259},[253,1098,294],{"class":259},[253,1100,1101],{"class":263}," NoteNotFound",[253,1103,512],{"class":300},[253,1105,981],{"class":259},[253,1107,1108],{"class":300}," id: ",[253,1110,669],{"class":263},[253,1112,907],{"class":300},[253,1114,556],{"class":263},[253,1116,396],{"class":300},[253,1118,1119,1121,1123,1125,1127,1129],{"class":255,"line":304},[253,1120,639],{"class":259},[253,1122,642],{"class":259},[253,1124,920],{"class":300},[253,1126,313],{"class":259},[253,1128,297],{"class":263},[253,1130,301],{"class":300},[253,1132,1133,1135,1137],{"class":255,"line":325},[253,1134,931],{"class":300},[253,1136,313],{"class":259},[253,1138,1139],{"class":300}," HttpStatusCode.NotFound,\n",[253,1141,1142,1144,1146,1149,1152,1155],{"class":255,"line":340},[253,1143,941],{"class":300},[253,1145,313],{"class":259},[253,1147,1148],{"class":515}," \"Note not found: ",[253,1150,1151],{"class":368},"$id",[253,1153,1154],{"class":515},"\"",[253,1156,337],{"class":300},[253,1158,1159],{"class":255,"line":353},[253,1160,959],{"class":300},[253,1162,1163],{"class":255,"line":374},[253,1164,413],{"class":300},[253,1166,1167],{"class":255,"line":379},[253,1168,279],{"emptyLinePlaceholder":278},[253,1170,1171],{"class":255,"line":384},[253,1172,1173],{"class":1057},"\u002F\u002F An ID parameter couldn't be parsed\n",[253,1175,1176,1178,1180,1183,1185,1187,1190,1192,1194,1196,1198],{"class":255,"line":399},[253,1177,291],{"class":259},[253,1179,294],{"class":259},[253,1181,1182],{"class":263}," IdMalformed",[253,1184,512],{"class":300},[253,1186,981],{"class":259},[253,1188,1189],{"class":259}," value",[253,1191,789],{"class":300},[253,1193,348],{"class":263},[253,1195,907],{"class":300},[253,1197,556],{"class":263},[253,1199,396],{"class":300},[253,1201,1202,1204,1206,1208,1210,1212],{"class":255,"line":410},[253,1203,639],{"class":259},[253,1205,642],{"class":259},[253,1207,920],{"class":300},[253,1209,313],{"class":259},[253,1211,297],{"class":263},[253,1213,301],{"class":300},[253,1215,1216,1218,1220],{"class":255,"line":416},[253,1217,931],{"class":300},[253,1219,313],{"class":259},[253,1221,1015],{"class":300},[253,1223,1224,1226,1228,1231,1234,1236],{"class":255,"line":421},[253,1225,941],{"class":300},[253,1227,313],{"class":259},[253,1229,1230],{"class":515}," \"ID is not a valid integer: ",[253,1232,1233],{"class":368},"$value",[253,1235,1154],{"class":515},[253,1237,337],{"class":300},[253,1239,1240,1243,1245,1247],{"class":255,"line":442},[253,1241,1242],{"class":300},"        fieldValue ",[253,1244,313],{"class":259},[253,1246,1189],{"class":259},[253,1248,337],{"class":300},[253,1250,1251],{"class":255,"line":447},[253,1252,959],{"class":300},[253,1254,1255],{"class":255,"line":482},[253,1256,413],{"class":300},[253,1258,1259],{"class":255,"line":497},[253,1260,279],{"emptyLinePlaceholder":278},[253,1262,1263],{"class":255,"line":528},[253,1264,1265],{"class":1057},"\u002F\u002F An upstream service call failed\n",[253,1267,1268,1270,1272,1275,1277,1280,1282,1284,1286],{"class":255,"line":545},[253,1269,291],{"class":259},[253,1271,294],{"class":259},[253,1273,1274],{"class":263}," StorageCallFailed",[253,1276,512],{"class":300},[253,1278,1279],{"class":259},"override",[253,1281,642],{"class":259},[253,1283,887],{"class":300},[253,1285,473],{"class":263},[253,1287,1288],{"class":300},") :\n",[253,1290,1291,1294,1297,1299,1302,1304,1307],{"class":255,"line":845},[253,1292,1293],{"class":263},"    UpstreamError",[253,1295,1296],{"class":300},"(upstream ",[253,1298,313],{"class":259},[253,1300,1301],{"class":300}," upstream, systemName ",[253,1303,313],{"class":259},[253,1305,1306],{"class":515}," \"Storage Service\"",[253,1308,322],{"class":300},[253,1310,1311],{"class":255,"line":850},[253,1312,279],{"emptyLinePlaceholder":278},[253,1314,1316],{"class":255,"line":1315},25,[253,1317,1318],{"class":1057},"\u002F\u002F The version file couldn't be read (covered later)\n",[253,1320,1322,1324,1326,1329,1331,1333,1336,1339,1341,1343],{"class":255,"line":1321},26,[253,1323,291],{"class":259},[253,1325,294],{"class":259},[253,1327,1328],{"class":263}," VersionNotReadable",[253,1330,512],{"class":300},[253,1332,981],{"class":259},[253,1334,1335],{"class":300}," e: ",[253,1337,1338],{"class":263},"Throwable",[253,1340,907],{"class":300},[253,1342,556],{"class":263},[253,1344,396],{"class":300},[253,1346,1348,1350,1352,1354,1356,1358],{"class":255,"line":1347},27,[253,1349,639],{"class":259},[253,1351,642],{"class":259},[253,1353,920],{"class":300},[253,1355,313],{"class":259},[253,1357,297],{"class":263},[253,1359,301],{"class":300},[253,1361,1363,1365,1367],{"class":255,"line":1362},28,[253,1364,931],{"class":300},[253,1366,313],{"class":259},[253,1368,936],{"class":300},[253,1370,1372,1374,1376,1379],{"class":255,"line":1371},29,[253,1373,941],{"class":300},[253,1375,313],{"class":259},[253,1377,1378],{"class":515}," \"${e.message}\"",[253,1380,337],{"class":300},[253,1382,1384],{"class":255,"line":1383},30,[253,1385,959],{"class":300},[253,1387,1389],{"class":255,"line":1388},31,[253,1390,413],{"class":300},[214,1392,1393,1394,1397,1398,1400],{},"The sealed interface means the compiler catches any unhandled case in ",[250,1395,1396],{},"when"," expressions, and adding a new error type\nmeans adding one data class — nothing else needs updating unless you want special ",[250,1399,560],{}," treatment.",[235,1402,1404],{"id":1403},"bridging-to-http-respondkt","Bridging to HTTP: Respond.kt",[214,1406,1407,1408,1411],{},"The second piece is a set of extension functions that translate ",[250,1409,1410],{},"Either\u003CApiError, A>"," directly into Ktor HTTP responses.\nKotlin's context parameters make these feel like they belong on the call:",[243,1413,1415],{"className":245,"code":1414,"language":247,"meta":248,"style":248},"import arrow.core.Either\nimport io.ktor.http.HttpStatusCode\nimport io.ktor.server.response.respond\nimport io.ktor.server.response.respondRedirect\nimport io.ktor.server.response.respondText\nimport io.ktor.server.routing.RoutingContext\n\ncontext(context: RoutingContext)\nsuspend inline fun \u003Creified A : Any> Either\u003CApiError, A>.respond(\n    status: HttpStatusCode = HttpStatusCode.OK,\n) {\n    onLeft { context.respond(it) }\n    onRight { context.call.respond(status, it) }\n}\n\ncontext(context: RoutingContext)\nsuspend fun Either\u003CApiError, String>.redirect(permanent: Boolean = false) {\n    onLeft { context.respond(it) }\n    onRight { context.call.respondRedirect(it, permanent) }\n}\n\ncontext(context: RoutingContext)\nsuspend fun Either\u003CApiError, String>.respondPlainText(status: HttpStatusCode = HttpStatusCode.OK) {\n    onLeft { context.respond(it) }\n    onRight { context.call.respondText(status = status, text = it) }\n}\n\nsuspend fun RoutingContext.respond(error: ApiError) =\n    call.respond(error.status(), error.messageMap())\n",[250,1416,1417,1424,1430,1437,1444,1451,1458,1462,1475,1519,1532,1536,1549,1562,1566,1570,1580,1614,1624,1636,1640,1644,1654,1683,1693,1715,1719,1723,1745],{"__ignoreMap":248},[253,1418,1419,1421],{"class":255,"line":256},[253,1420,260],{"class":259},[253,1422,1423],{"class":263}," arrow.core.Either\n",[253,1425,1426,1428],{"class":255,"line":267},[253,1427,260],{"class":259},[253,1429,264],{"class":263},[253,1431,1432,1434],{"class":255,"line":275},[253,1433,260],{"class":259},[253,1435,1436],{"class":263}," io.ktor.server.response.respond\n",[253,1438,1439,1441],{"class":255,"line":282},[253,1440,260],{"class":259},[253,1442,1443],{"class":263}," io.ktor.server.response.respondRedirect\n",[253,1445,1446,1448],{"class":255,"line":288},[253,1447,260],{"class":259},[253,1449,1450],{"class":263}," io.ktor.server.response.respondText\n",[253,1452,1453,1455],{"class":255,"line":304},[253,1454,260],{"class":259},[253,1456,1457],{"class":263}," io.ktor.server.routing.RoutingContext\n",[253,1459,1460],{"class":255,"line":325},[253,1461,279],{"emptyLinePlaceholder":278},[253,1463,1464,1467,1470,1473],{"class":255,"line":340},[253,1465,1466],{"class":263},"context",[253,1468,1469],{"class":300},"(context: ",[253,1471,1472],{"class":263},"RoutingContext",[253,1474,322],{"class":300},[253,1476,1477,1480,1483,1485,1488,1491,1494,1496,1499,1501,1504,1506,1509,1512,1514,1517],{"class":255,"line":353},[253,1478,1479],{"class":259},"suspend",[253,1481,1482],{"class":259}," inline",[253,1484,708],{"class":259},[253,1486,1487],{"class":300}," \u003C",[253,1489,1490],{"class":263},"reified",[253,1492,1493],{"class":263}," A",[253,1495,624],{"class":300},[253,1497,1498],{"class":263},"Any",[253,1500,476],{"class":300},[253,1502,1503],{"class":263},"Either",[253,1505,465],{"class":259},[253,1507,1508],{"class":300},"ApiError, A",[253,1510,1511],{"class":259},">",[253,1513,233],{"class":300},[253,1515,1516],{"class":263},"respond",[253,1518,301],{"class":300},[253,1520,1521,1524,1526,1529],{"class":255,"line":374},[253,1522,1523],{"class":300},"    status: ",[253,1525,334],{"class":263},[253,1527,1528],{"class":259}," =",[253,1530,1531],{"class":300}," HttpStatusCode.OK,\n",[253,1533,1534],{"class":255,"line":379},[253,1535,494],{"class":300},[253,1537,1538,1541,1544,1546],{"class":255,"line":384},[253,1539,1540],{"class":263},"    onLeft",[253,1542,1543],{"class":300}," { context.",[253,1545,1516],{"class":263},[253,1547,1548],{"class":300},"(it) }\n",[253,1550,1551,1554,1557,1559],{"class":255,"line":399},[253,1552,1553],{"class":263},"    onRight",[253,1555,1556],{"class":300}," { context.call.",[253,1558,1516],{"class":263},[253,1560,1561],{"class":300},"(status, it) }\n",[253,1563,1564],{"class":255,"line":410},[253,1565,413],{"class":300},[253,1567,1568],{"class":255,"line":416},[253,1569,279],{"emptyLinePlaceholder":278},[253,1571,1572,1574,1576,1578],{"class":255,"line":421},[253,1573,1466],{"class":263},[253,1575,1469],{"class":300},[253,1577,1472],{"class":263},[253,1579,322],{"class":300},[253,1581,1582,1584,1586,1589,1591,1594,1596,1598,1601,1604,1607,1609,1612],{"class":255,"line":442},[253,1583,1479],{"class":259},[253,1585,708],{"class":259},[253,1587,1588],{"class":263}," Either",[253,1590,465],{"class":259},[253,1592,1593],{"class":300},"ApiError, String",[253,1595,1511],{"class":259},[253,1597,233],{"class":300},[253,1599,1600],{"class":263},"redirect",[253,1602,1603],{"class":300},"(permanent: ",[253,1605,1606],{"class":263},"Boolean",[253,1608,1528],{"class":259},[253,1610,1611],{"class":368}," false",[253,1613,494],{"class":300},[253,1615,1616,1618,1620,1622],{"class":255,"line":447},[253,1617,1540],{"class":263},[253,1619,1543],{"class":300},[253,1621,1516],{"class":263},[253,1623,1548],{"class":300},[253,1625,1626,1628,1630,1633],{"class":255,"line":482},[253,1627,1553],{"class":263},[253,1629,1556],{"class":300},[253,1631,1632],{"class":263},"respondRedirect",[253,1634,1635],{"class":300},"(it, permanent) }\n",[253,1637,1638],{"class":255,"line":497},[253,1639,413],{"class":300},[253,1641,1642],{"class":255,"line":528},[253,1643,279],{"emptyLinePlaceholder":278},[253,1645,1646,1648,1650,1652],{"class":255,"line":545},[253,1647,1466],{"class":263},[253,1649,1469],{"class":300},[253,1651,1472],{"class":263},[253,1653,322],{"class":300},[253,1655,1656,1658,1660,1662,1664,1666,1668,1670,1673,1676,1678,1680],{"class":255,"line":845},[253,1657,1479],{"class":259},[253,1659,708],{"class":259},[253,1661,1588],{"class":263},[253,1663,465],{"class":259},[253,1665,1593],{"class":300},[253,1667,1511],{"class":259},[253,1669,233],{"class":300},[253,1671,1672],{"class":263},"respondPlainText",[253,1674,1675],{"class":300},"(status: ",[253,1677,334],{"class":263},[253,1679,1528],{"class":259},[253,1681,1682],{"class":300}," HttpStatusCode.OK) {\n",[253,1684,1685,1687,1689,1691],{"class":255,"line":850},[253,1686,1540],{"class":263},[253,1688,1543],{"class":300},[253,1690,1516],{"class":263},[253,1692,1548],{"class":300},[253,1694,1695,1697,1699,1702,1705,1707,1710,1712],{"class":255,"line":1315},[253,1696,1553],{"class":263},[253,1698,1556],{"class":300},[253,1700,1701],{"class":263},"respondText",[253,1703,1704],{"class":300},"(status ",[253,1706,313],{"class":259},[253,1708,1709],{"class":300}," status, text ",[253,1711,313],{"class":259},[253,1713,1714],{"class":300}," it) }\n",[253,1716,1717],{"class":255,"line":1321},[253,1718,413],{"class":300},[253,1720,1721],{"class":255,"line":1347},[253,1722,279],{"emptyLinePlaceholder":278},[253,1724,1725,1727,1729,1732,1734,1736,1739,1741,1743],{"class":255,"line":1362},[253,1726,1479],{"class":259},[253,1728,708],{"class":259},[253,1730,1731],{"class":263}," RoutingContext",[253,1733,233],{"class":300},[253,1735,1516],{"class":263},[253,1737,1738],{"class":300},"(error: ",[253,1740,556],{"class":263},[253,1742,794],{"class":300},[253,1744,479],{"class":259},[253,1746,1747,1750,1752,1755,1757,1760,1762],{"class":255,"line":1371},[253,1748,1749],{"class":300},"    call.",[253,1751,1516],{"class":263},[253,1753,1754],{"class":300},"(error.",[253,1756,431],{"class":263},[253,1758,1759],{"class":300},"(), error.",[253,1761,456],{"class":263},[253,1763,1764],{"class":300},"())\n",[214,1766,1767,1768,1771,1772,1775,1776,1771,1779,1782,1783,1786,1787,1790,1791,1794,1795,1797,1798,1800],{},"There is a discussion here between ",[250,1769,1770],{},"onLeft","\u002F",[250,1773,1774],{},"onRight",", vs ",[250,1777,1778],{},"ifLeft",[250,1780,1781],{},"ifRight",". Both can work - but I have chosen ",[250,1784,1785],{},"on*",". The\n",[250,1788,1789],{},"if*"," variants are for side effects that return ",[250,1792,1793],{},"Unit","; the ",[250,1796,1785],{}," variants return the original ",[250,1799,1503],{},", which keeps the\ntype flowing correctly through the chain.",[214,1802,1803,1804,1807,1808,1811,1812,1815,1816,1819,1820,233],{},"Routes call ",[250,1805,1806],{},".respond()"," on the result of an ",[250,1809,1810],{},"either { }"," block. If the block produces a ",[250,1813,1814],{},"Left\u003CApiError>",", the error is\nserialized and sent with the appropriate HTTP status. If it produces a ",[250,1817,1818],{},"Right\u003CA>",", the value is serialized with a 200 (\nor whatever status was passed). The route handler is left with nothing to do but call ",[250,1821,1806],{},[235,1823,1825],{"id":1824},"service-layer-with-raise","Service Layer with Raise",[214,1827,1828,1829,1832,1833,1836,1837,470,1840,1843,1844,1847],{},"Arrow's ",[250,1830,1831],{},"Raise\u003CE>"," interface is what makes errors propagate without exceptions. A function that declares\n",[250,1834,1835],{},"context(_: Raise\u003CApiError>)"," can call ",[250,1838,1839],{},"raise()",[250,1841,1842],{},"ensure()",", and ",[250,1845,1846],{},"ensureNotNull()"," directly, and any error\nshort-circuits out of the function just like a thrown exception — but typed, not thrown.",[214,1849,1850],{},"Here's a service for the note-taking example:",[243,1852,1854],{"className":245,"code":1853,"language":247,"meta":248,"style":248},"import arrow.core.raise.Raise\nimport arrow.core.raise.ensure\nimport arrow.core.raise.ensureNotNull\n\nclass NoteService(private val repository: NoteRepository) {\n\n    context(_: Raise\u003CApiError>)\n    suspend fun getNote(id: Int): Note {\n        return ensureNotNull(repository.findById(id)) { NoteNotFound(id) }\n    }\n\n    context(_: Raise\u003CApiError>)\n    suspend fun createNote(request: CreateNoteRequest): Note {\n        ensure(request.title.isNotBlank()) { TitleRequired }\n        return repository.save(Note(title = request.title, body = request.body))\n    }\n\n    context(_: Raise\u003CApiError>)\n    suspend fun listNotes(tag: String?): List\u003CNote> {\n        val notes = repository.findAll()\n        return if (tag != null) {\n            ensure(notes.any { tag in it.tags }) { TagNotFound(tag) }\n            notes.filter { tag in it.tags }\n        } else {\n            notes\n        }\n    }\n}\n",[250,1855,1856,1863,1870,1877,1881,1903,1907,1925,1947,1970,1974,1978,1992,2013,2027,2054,2058,2062,2076,2102,2120,2137,2163,2178,2188,2193,2197,2201],{"__ignoreMap":248},[253,1857,1858,1860],{"class":255,"line":256},[253,1859,260],{"class":259},[253,1861,1862],{"class":263}," arrow.core.raise.Raise\n",[253,1864,1865,1867],{"class":255,"line":267},[253,1866,260],{"class":259},[253,1868,1869],{"class":263}," arrow.core.raise.ensure\n",[253,1871,1872,1874],{"class":255,"line":275},[253,1873,260],{"class":259},[253,1875,1876],{"class":263}," arrow.core.raise.ensureNotNull\n",[253,1878,1879],{"class":255,"line":282},[253,1880,279],{"emptyLinePlaceholder":278},[253,1882,1883,1885,1888,1890,1893,1895,1898,1901],{"class":255,"line":288},[253,1884,319],{"class":259},[253,1886,1887],{"class":263}," NoteService",[253,1889,512],{"class":300},[253,1891,1892],{"class":259},"private",[253,1894,642],{"class":259},[253,1896,1897],{"class":300}," repository: ",[253,1899,1900],{"class":263},"NoteRepository",[253,1902,494],{"class":300},[253,1904,1905],{"class":255,"line":304},[253,1906,279],{"emptyLinePlaceholder":278},[253,1908,1909,1912,1915,1918,1920,1922],{"class":255,"line":325},[253,1910,1911],{"class":263},"    context",[253,1913,1914],{"class":300},"(_: ",[253,1916,1917],{"class":263},"Raise",[253,1919,465],{"class":300},[253,1921,556],{"class":263},[253,1923,1924],{"class":300},">)\n",[253,1926,1927,1930,1932,1935,1938,1940,1942,1945],{"class":255,"line":340},[253,1928,1929],{"class":259},"    suspend",[253,1931,708],{"class":259},[253,1933,1934],{"class":263}," getNote",[253,1936,1937],{"class":300},"(id: ",[253,1939,669],{"class":263},[253,1941,720],{"class":300},[253,1943,1944],{"class":263},"Note",[253,1946,396],{"class":300},[253,1948,1949,1952,1955,1958,1961,1964,1967],{"class":255,"line":353},[253,1950,1951],{"class":259},"        return",[253,1953,1954],{"class":263}," ensureNotNull",[253,1956,1957],{"class":300},"(repository.",[253,1959,1960],{"class":263},"findById",[253,1962,1963],{"class":300},"(id)) { ",[253,1965,1966],{"class":263},"NoteNotFound",[253,1968,1969],{"class":300},"(id) }\n",[253,1971,1972],{"class":255,"line":374},[253,1973,548],{"class":300},[253,1975,1976],{"class":255,"line":379},[253,1977,279],{"emptyLinePlaceholder":278},[253,1979,1980,1982,1984,1986,1988,1990],{"class":255,"line":384},[253,1981,1911],{"class":263},[253,1983,1914],{"class":300},[253,1985,1917],{"class":263},[253,1987,465],{"class":300},[253,1989,556],{"class":263},[253,1991,1924],{"class":300},[253,1993,1994,1996,1998,2001,2004,2007,2009,2011],{"class":255,"line":399},[253,1995,1929],{"class":259},[253,1997,708],{"class":259},[253,1999,2000],{"class":263}," createNote",[253,2002,2003],{"class":300},"(request: ",[253,2005,2006],{"class":263},"CreateNoteRequest",[253,2008,720],{"class":300},[253,2010,1944],{"class":263},[253,2012,396],{"class":300},[253,2014,2015,2018,2021,2024],{"class":255,"line":410},[253,2016,2017],{"class":263},"        ensure",[253,2019,2020],{"class":300},"(request.title.",[253,2022,2023],{"class":263},"isNotBlank",[253,2025,2026],{"class":300},"()) { TitleRequired }\n",[253,2028,2029,2031,2034,2037,2039,2041,2044,2046,2049,2051],{"class":255,"line":416},[253,2030,1951],{"class":259},[253,2032,2033],{"class":300}," repository.",[253,2035,2036],{"class":263},"save",[253,2038,512],{"class":300},[253,2040,1944],{"class":263},[253,2042,2043],{"class":300},"(title ",[253,2045,313],{"class":259},[253,2047,2048],{"class":300}," request.title, body ",[253,2050,313],{"class":259},[253,2052,2053],{"class":300}," request.body))\n",[253,2055,2056],{"class":255,"line":421},[253,2057,548],{"class":300},[253,2059,2060],{"class":255,"line":442},[253,2061,279],{"emptyLinePlaceholder":278},[253,2063,2064,2066,2068,2070,2072,2074],{"class":255,"line":447},[253,2065,1911],{"class":263},[253,2067,1914],{"class":300},[253,2069,1917],{"class":263},[253,2071,465],{"class":300},[253,2073,556],{"class":263},[253,2075,1924],{"class":300},[253,2077,2078,2080,2082,2085,2088,2090,2093,2096,2098,2100],{"class":255,"line":482},[253,2079,1929],{"class":259},[253,2081,708],{"class":259},[253,2083,2084],{"class":263}," listNotes",[253,2086,2087],{"class":300},"(tag: ",[253,2089,348],{"class":263},[253,2091,2092],{"class":300},"?): ",[253,2094,2095],{"class":263},"List",[253,2097,465],{"class":300},[253,2099,1944],{"class":263},[253,2101,634],{"class":300},[253,2103,2104,2107,2110,2112,2114,2117],{"class":255,"line":497},[253,2105,2106],{"class":259},"        val",[253,2108,2109],{"class":300}," notes ",[253,2111,313],{"class":259},[253,2113,2033],{"class":300},[253,2115,2116],{"class":263},"findAll",[253,2118,2119],{"class":300},"()\n",[253,2121,2122,2124,2127,2130,2133,2135],{"class":255,"line":528},[253,2123,1951],{"class":259},[253,2125,2126],{"class":259}," if",[253,2128,2129],{"class":300}," (tag ",[253,2131,2132],{"class":259},"!=",[253,2134,369],{"class":368},[253,2136,494],{"class":300},[253,2138,2139,2142,2145,2148,2151,2154,2157,2160],{"class":255,"line":545},[253,2140,2141],{"class":263},"            ensure",[253,2143,2144],{"class":300},"(notes.",[253,2146,2147],{"class":263},"any",[253,2149,2150],{"class":300}," { tag ",[253,2152,2153],{"class":259},"in",[253,2155,2156],{"class":300}," it.tags }) { ",[253,2158,2159],{"class":263},"TagNotFound",[253,2161,2162],{"class":300},"(tag) }\n",[253,2164,2165,2168,2171,2173,2175],{"class":255,"line":845},[253,2166,2167],{"class":300},"            notes.",[253,2169,2170],{"class":263},"filter",[253,2172,2150],{"class":300},[253,2174,2153],{"class":259},[253,2176,2177],{"class":300}," it.tags }\n",[253,2179,2180,2183,2186],{"class":255,"line":850},[253,2181,2182],{"class":300},"        } ",[253,2184,2185],{"class":259},"else",[253,2187,396],{"class":300},[253,2189,2190],{"class":255,"line":1315},[253,2191,2192],{"class":300},"            notes\n",[253,2194,2195],{"class":255,"line":1321},[253,2196,697],{"class":300},[253,2198,2199],{"class":255,"line":1347},[253,2200,548],{"class":300},[253,2202,2203],{"class":255,"line":1362},[253,2204,413],{"class":300},[214,2206,2207,2210,2211,2214,2215,2218],{},[250,2208,2209],{},"ensureNotNull"," unwraps a nullable value or raises the given error. ",[250,2212,2213],{},"ensure"," raises if the condition is false — it reads\nalmost like a precondition check. Neither requires a try-catch or a manual ",[250,2216,2217],{},"if"," branch; they just short-circuit.",[854,2220,2222],{"id":2221},"raise-for-explicit-errors","raise() for explicit errors",[214,2224,2225,2227],{},[250,2226,1839],{}," is the explicit form — use it anywhere you want to abort with an error:",[243,2229,2231],{"className":245,"code":2230,"language":247,"meta":248,"style":248},"context(_: Raise\u003CApiError>)\nsuspend fun processNote(id: Int): ProcessedNote {\n    val note = repository.findById(id) ?: raise(NoteNotFound(id))\n    if (note.archived) raise(NoteArchived(id))\n    return process(note)\n}\n",[250,2232,2233,2247,2267,2293,2310,2321],{"__ignoreMap":248},[253,2234,2235,2237,2239,2241,2243,2245],{"class":255,"line":256},[253,2236,1466],{"class":263},[253,2238,1914],{"class":300},[253,2240,1917],{"class":263},[253,2242,465],{"class":300},[253,2244,556],{"class":263},[253,2246,1924],{"class":300},[253,2248,2249,2251,2253,2256,2258,2260,2262,2265],{"class":255,"line":267},[253,2250,1479],{"class":259},[253,2252,708],{"class":259},[253,2254,2255],{"class":263}," processNote",[253,2257,1937],{"class":300},[253,2259,669],{"class":263},[253,2261,720],{"class":300},[253,2263,2264],{"class":263},"ProcessedNote",[253,2266,396],{"class":300},[253,2268,2269,2271,2274,2276,2278,2280,2283,2286,2288,2290],{"class":255,"line":275},[253,2270,328],{"class":259},[253,2272,2273],{"class":300}," note ",[253,2275,313],{"class":259},[253,2277,2033],{"class":300},[253,2279,1960],{"class":263},[253,2281,2282],{"class":300},"(id) ?: ",[253,2284,2285],{"class":263},"raise",[253,2287,512],{"class":300},[253,2289,1966],{"class":263},[253,2291,2292],{"class":300},"(id))\n",[253,2294,2295,2298,2301,2303,2305,2308],{"class":255,"line":282},[253,2296,2297],{"class":259},"    if",[253,2299,2300],{"class":300}," (note.archived) ",[253,2302,2285],{"class":263},[253,2304,512],{"class":300},[253,2306,2307],{"class":263},"NoteArchived",[253,2309,2292],{"class":300},[253,2311,2312,2315,2318],{"class":255,"line":288},[253,2313,2314],{"class":259},"    return",[253,2316,2317],{"class":263}," process",[253,2319,2320],{"class":300},"(note)\n",[253,2322,2323],{"class":255,"line":304},[253,2324,413],{"class":300},[214,2326,2327,2328,2331,2332,2334,2335,2338],{},"It's equivalent to ",[250,2329,2330],{},"throw"," for this error type, but unlike exceptions the compiler enforces it — you can only call\n",[250,2333,1839],{}," from within a ",[250,2336,2337],{},"Raise\u003CApiError>"," context, so the error can't silently escape.",[854,2340,2342],{"id":2341},"wrapping-exceptions-catch-and-witherror","Wrapping exceptions: catch and withError",[214,2344,2345,2346,2349,2350,2353],{},"External calls — HTTP clients, database drivers, file I\u002FO — throw exceptions. Arrow's ",[250,2347,2348],{},"catch"," and ",[250,2351,2352],{},"withError"," let you\nabsorb those at the boundary and convert them into typed errors.",[214,2355,2356,2358,2359,2361],{},[250,2357,2348],{}," runs a block and intercepts any ",[250,2360,1338],{},":",[243,2363,2365],{"className":245,"code":2364,"language":247,"meta":248,"style":248},"import arrow.core.raise.catch\n\ncontext(_: Raise\u003CApiError>)\nsuspend fun fetchRemoteData(url: String): RemoteData {\n    return catch({\n        httpClient.get(url).body\u003CRemoteData>()\n    }) { throwable ->\n        raise(StorageCallFailed(ErrorResponse(HttpStatusCode.InternalServerError, throwable.message ?: \"unknown\")))\n    }\n}\n",[250,2366,2367,2374,2378,2392,2413,2423,2444,2452,2475,2479],{"__ignoreMap":248},[253,2368,2369,2371],{"class":255,"line":256},[253,2370,260],{"class":259},[253,2372,2373],{"class":263}," arrow.core.raise.catch\n",[253,2375,2376],{"class":255,"line":267},[253,2377,279],{"emptyLinePlaceholder":278},[253,2379,2380,2382,2384,2386,2388,2390],{"class":255,"line":275},[253,2381,1466],{"class":263},[253,2383,1914],{"class":300},[253,2385,1917],{"class":263},[253,2387,465],{"class":300},[253,2389,556],{"class":263},[253,2391,1924],{"class":300},[253,2393,2394,2396,2398,2401,2404,2406,2408,2411],{"class":255,"line":282},[253,2395,1479],{"class":259},[253,2397,708],{"class":259},[253,2399,2400],{"class":263}," fetchRemoteData",[253,2402,2403],{"class":300},"(url: ",[253,2405,348],{"class":263},[253,2407,720],{"class":300},[253,2409,2410],{"class":263},"RemoteData",[253,2412,396],{"class":300},[253,2414,2415,2417,2420],{"class":255,"line":288},[253,2416,2314],{"class":259},[253,2418,2419],{"class":259}," catch",[253,2421,2422],{"class":300},"({\n",[253,2424,2425,2428,2431,2434,2437,2439,2441],{"class":255,"line":304},[253,2426,2427],{"class":300},"        httpClient.",[253,2429,2430],{"class":263},"get",[253,2432,2433],{"class":300},"(url).",[253,2435,2436],{"class":263},"body",[253,2438,465],{"class":300},[253,2440,2410],{"class":263},[253,2442,2443],{"class":300},">()\n",[253,2445,2446,2449],{"class":255,"line":325},[253,2447,2448],{"class":300},"    }) { throwable ",[253,2450,2451],{"class":259},"->\n",[253,2453,2454,2457,2459,2462,2464,2466,2469,2472],{"class":255,"line":340},[253,2455,2456],{"class":263},"        raise",[253,2458,512],{"class":300},[253,2460,2461],{"class":263},"StorageCallFailed",[253,2463,512],{"class":300},[253,2465,473],{"class":263},[253,2467,2468],{"class":300},"(HttpStatusCode.InternalServerError, throwable.message ?: ",[253,2470,2471],{"class":515},"\"unknown\"",[253,2473,2474],{"class":300},")))\n",[253,2476,2477],{"class":255,"line":353},[253,2478,548],{"class":300},[253,2480,2481],{"class":255,"line":374},[253,2482,413],{"class":300},[214,2484,2485,2487,2488,2490,2491,2361],{},[250,2486,2352],{}," transforms the error type from one ",[250,2489,1917],{}," context to another. This is useful when you have a function that\nraises a specific exception type and you want to map it to your ",[250,2492,556],{},[243,2494,2496],{"className":245,"code":2495,"language":247,"meta":248,"style":248},"import arrow.core.raise.withError\n\ncontext(_: Raise\u003CApiError>)\nfun readConfig(path: String): Config =\n    withError({ e: Throwable -> ConfigReadFailed(e.message ?: \"read error\") }) {\n        catch({\n            Config.parse(File(path).readText())\n        }) { raise(it) }\n    }\n",[250,2497,2498,2505,2509,2523,2542,2567,2574,2595,2604],{"__ignoreMap":248},[253,2499,2500,2502],{"class":255,"line":256},[253,2501,260],{"class":259},[253,2503,2504],{"class":263}," arrow.core.raise.withError\n",[253,2506,2507],{"class":255,"line":267},[253,2508,279],{"emptyLinePlaceholder":278},[253,2510,2511,2513,2515,2517,2519,2521],{"class":255,"line":275},[253,2512,1466],{"class":263},[253,2514,1914],{"class":300},[253,2516,1917],{"class":263},[253,2518,465],{"class":300},[253,2520,556],{"class":263},[253,2522,1924],{"class":300},[253,2524,2525,2527,2530,2533,2535,2537,2540],{"class":255,"line":282},[253,2526,424],{"class":259},[253,2528,2529],{"class":263}," readConfig",[253,2531,2532],{"class":300},"(path: ",[253,2534,348],{"class":263},[253,2536,720],{"class":300},[253,2538,2539],{"class":263},"Config",[253,2541,725],{"class":259},[253,2543,2544,2547,2550,2552,2555,2558,2561,2564],{"class":255,"line":288},[253,2545,2546],{"class":263},"    withError",[253,2548,2549],{"class":300},"({ e: ",[253,2551,1338],{"class":263},[253,2553,2554],{"class":300}," -> ",[253,2556,2557],{"class":263},"ConfigReadFailed",[253,2559,2560],{"class":300},"(e.message ?: ",[253,2562,2563],{"class":515},"\"read error\"",[253,2565,2566],{"class":300},") }) {\n",[253,2568,2569,2572],{"class":255,"line":304},[253,2570,2571],{"class":259},"        catch",[253,2573,2422],{"class":300},[253,2575,2576,2579,2582,2584,2587,2590,2593],{"class":255,"line":325},[253,2577,2578],{"class":300},"            Config.",[253,2580,2581],{"class":263},"parse",[253,2583,512],{"class":300},[253,2585,2586],{"class":263},"File",[253,2588,2589],{"class":300},"(path).",[253,2591,2592],{"class":263},"readText",[253,2594,1764],{"class":300},[253,2596,2597,2600,2602],{"class":255,"line":340},[253,2598,2599],{"class":300},"        }) { ",[253,2601,2285],{"class":263},[253,2603,1548],{"class":300},[253,2605,2606],{"class":255,"line":353},[253,2607,548],{"class":300},[214,2609,2610,2611,2613,2614,2616,2617,2619],{},"The two are often combined: ",[250,2612,2352],{}," sets the error mapping, and ",[250,2615,2348],{}," absorbs the thrown exception and calls\n",[250,2618,2285],{}," with it. The exception never propagates; it becomes a typed value.",[235,2621,2623],{"id":2622},"routing-either-respond","Routing: either { }.respond()",[214,2625,2626,2627,2629,2630,2633],{},"With ",[250,2628,1917],{},"-aware services and ",[250,2631,2632],{},"Either.respond()"," in place, route handlers become very thin:",[243,2635,2637],{"className":245,"code":2636,"language":247,"meta":248,"style":248},"fun Application.configureNoteRouting(service: NoteService) {\n    routing {\n        route(\"\u002Fapi\u002Fnotes\") {\n            get {\n                val tag = call.request.queryParameters[\"tag\"]\n                either {\n                    service.listNotes(tag)\n                }.respond()\n            }\n\n            get(\"\u002F{id}\") {\n                either {\n                    val id = NoteId(call.parameters[\"id\"]).bind()\n                    service.getNote(id.value)\n                }.respond()\n            }\n\n            post {\n                either {\n                    val request = call.receive\u003CCreateNoteRequest>()\n                    service.createNote(request)\n                }.respond(HttpStatusCode.Created)\n            }\n        }\n    }\n}\n",[250,2638,2639,2659,2666,2678,2685,2704,2711,2722,2731,2736,2740,2751,2757,2784,2798,2806,2810,2814,2821,2827,2848,2858,2867,2871,2875,2879],{"__ignoreMap":248},[253,2640,2641,2643,2646,2648,2651,2654,2657],{"class":255,"line":256},[253,2642,424],{"class":259},[253,2644,2645],{"class":263}," Application",[253,2647,233],{"class":300},[253,2649,2650],{"class":263},"configureNoteRouting",[253,2652,2653],{"class":300},"(service: ",[253,2655,2656],{"class":263},"NoteService",[253,2658,494],{"class":300},[253,2660,2661,2664],{"class":255,"line":267},[253,2662,2663],{"class":263},"    routing",[253,2665,396],{"class":300},[253,2667,2668,2671,2673,2676],{"class":255,"line":275},[253,2669,2670],{"class":263},"        route",[253,2672,512],{"class":300},[253,2674,2675],{"class":515},"\"\u002Fapi\u002Fnotes\"",[253,2677,494],{"class":300},[253,2679,2680,2683],{"class":255,"line":282},[253,2681,2682],{"class":259},"            get",[253,2684,396],{"class":300},[253,2686,2687,2690,2693,2695,2698,2701],{"class":255,"line":288},[253,2688,2689],{"class":259},"                val",[253,2691,2692],{"class":300}," tag ",[253,2694,313],{"class":259},[253,2696,2697],{"class":300}," call.request.queryParameters[",[253,2699,2700],{"class":515},"\"tag\"",[253,2702,2703],{"class":300},"]\n",[253,2705,2706,2709],{"class":255,"line":304},[253,2707,2708],{"class":263},"                either",[253,2710,396],{"class":300},[253,2712,2713,2716,2719],{"class":255,"line":325},[253,2714,2715],{"class":300},"                    service.",[253,2717,2718],{"class":263},"listNotes",[253,2720,2721],{"class":300},"(tag)\n",[253,2723,2724,2727,2729],{"class":255,"line":340},[253,2725,2726],{"class":300},"                }.",[253,2728,1516],{"class":263},[253,2730,2119],{"class":300},[253,2732,2733],{"class":255,"line":353},[253,2734,2735],{"class":300},"            }\n",[253,2737,2738],{"class":255,"line":374},[253,2739,279],{"emptyLinePlaceholder":278},[253,2741,2742,2744,2746,2749],{"class":255,"line":379},[253,2743,2682],{"class":259},[253,2745,512],{"class":300},[253,2747,2748],{"class":515},"\"\u002F{id}\"",[253,2750,494],{"class":300},[253,2752,2753,2755],{"class":255,"line":384},[253,2754,2708],{"class":263},[253,2756,396],{"class":300},[253,2758,2759,2762,2765,2767,2770,2773,2776,2779,2782],{"class":255,"line":399},[253,2760,2761],{"class":259},"                    val",[253,2763,2764],{"class":300}," id ",[253,2766,313],{"class":259},[253,2768,2769],{"class":263}," NoteId",[253,2771,2772],{"class":300},"(call.parameters[",[253,2774,2775],{"class":515},"\"id\"",[253,2777,2778],{"class":300},"]).",[253,2780,2781],{"class":263},"bind",[253,2783,2119],{"class":300},[253,2785,2786,2788,2791,2794,2796],{"class":255,"line":410},[253,2787,2715],{"class":300},[253,2789,2790],{"class":263},"getNote",[253,2792,2793],{"class":300},"(id.",[253,2795,786],{"class":259},[253,2797,322],{"class":300},[253,2799,2800,2802,2804],{"class":255,"line":416},[253,2801,2726],{"class":300},[253,2803,1516],{"class":263},[253,2805,2119],{"class":300},[253,2807,2808],{"class":255,"line":421},[253,2809,2735],{"class":300},[253,2811,2812],{"class":255,"line":442},[253,2813,279],{"emptyLinePlaceholder":278},[253,2815,2816,2819],{"class":255,"line":447},[253,2817,2818],{"class":263},"            post",[253,2820,396],{"class":300},[253,2822,2823,2825],{"class":255,"line":482},[253,2824,2708],{"class":263},[253,2826,396],{"class":300},[253,2828,2829,2831,2834,2836,2839,2842,2844,2846],{"class":255,"line":497},[253,2830,2761],{"class":259},[253,2832,2833],{"class":300}," request ",[253,2835,313],{"class":259},[253,2837,2838],{"class":300}," call.",[253,2840,2841],{"class":263},"receive",[253,2843,465],{"class":300},[253,2845,2006],{"class":263},[253,2847,2443],{"class":300},[253,2849,2850,2852,2855],{"class":255,"line":528},[253,2851,2715],{"class":300},[253,2853,2854],{"class":263},"createNote",[253,2856,2857],{"class":300},"(request)\n",[253,2859,2860,2862,2864],{"class":255,"line":545},[253,2861,2726],{"class":300},[253,2863,1516],{"class":263},[253,2865,2866],{"class":300},"(HttpStatusCode.Created)\n",[253,2868,2869],{"class":255,"line":845},[253,2870,2735],{"class":300},[253,2872,2873],{"class":255,"line":850},[253,2874,697],{"class":300},[253,2876,2877],{"class":255,"line":1315},[253,2878,548],{"class":300},[253,2880,2881],{"class":255,"line":1321},[253,2882,413],{"class":300},[214,2884,2885,2887,2888,2890,2891,2893,2894,2897,2898,2900,2901,2904,2905,2908],{},[250,2886,1810],{}," converts a ",[250,2889,1917],{},"-based computation into an ",[250,2892,1503],{},". Inside the block, ",[250,2895,2896],{},".bind()"," lifts an ",[250,2899,1503],{},"\nvalue — if it's a ",[250,2902,2903],{},"Left",", the block short-circuits with that error; if it's a ",[250,2906,2907],{},"Right",", you get the unwrapped value. This\nis how validated domain objects slot in:",[243,2910,2912],{"className":245,"code":2911,"language":247,"meta":248,"style":248},"data class NoteId(val value: Int) {\n    companion object {\n        operator fun invoke(raw: String?): Either\u003CApiError, NoteId> = either {\n            val present = ensureNotNull(raw?.takeIf { it.isNotBlank() }) { IdRequired }\n            val parsed = present.toIntOrNull() ?: raise(IdMalformed(present))\n            NoteId(parsed)\n        }\n    }\n}\n",[250,2913,2914,2934,2943,2980,3006,3034,3042,3046,3050],{"__ignoreMap":248},[253,2915,2916,2918,2920,2922,2924,2926,2928,2930,2932],{"class":255,"line":256},[253,2917,291],{"class":259},[253,2919,294],{"class":259},[253,2921,2769],{"class":263},[253,2923,512],{"class":300},[253,2925,981],{"class":259},[253,2927,1189],{"class":259},[253,2929,789],{"class":300},[253,2931,669],{"class":263},[253,2933,494],{"class":300},[253,2935,2936,2939,2941],{"class":255,"line":267},[253,2937,2938],{"class":259},"    companion",[253,2940,1065],{"class":259},[253,2942,396],{"class":300},[253,2944,2945,2948,2950,2953,2956,2958,2960,2962,2964,2966,2968,2971,2973,2975,2978],{"class":255,"line":275},[253,2946,2947],{"class":259},"        operator",[253,2949,708],{"class":259},[253,2951,2952],{"class":263}," invoke",[253,2954,2955],{"class":300},"(raw: ",[253,2957,348],{"class":263},[253,2959,2092],{"class":300},[253,2961,1503],{"class":263},[253,2963,465],{"class":300},[253,2965,556],{"class":263},[253,2967,470],{"class":300},[253,2969,2970],{"class":263},"NoteId",[253,2972,476],{"class":300},[253,2974,313],{"class":259},[253,2976,2977],{"class":263}," either",[253,2979,396],{"class":300},[253,2981,2982,2985,2988,2990,2992,2995,2998,3001,3003],{"class":255,"line":282},[253,2983,2984],{"class":259},"            val",[253,2986,2987],{"class":300}," present ",[253,2989,313],{"class":259},[253,2991,1954],{"class":263},[253,2993,2994],{"class":300},"(raw?.",[253,2996,2997],{"class":263},"takeIf",[253,2999,3000],{"class":300}," { it.",[253,3002,2023],{"class":263},[253,3004,3005],{"class":300},"() }) { IdRequired }\n",[253,3007,3008,3010,3013,3015,3018,3021,3024,3026,3028,3031],{"class":255,"line":288},[253,3009,2984],{"class":259},[253,3011,3012],{"class":300}," parsed ",[253,3014,313],{"class":259},[253,3016,3017],{"class":300}," present.",[253,3019,3020],{"class":263},"toIntOrNull",[253,3022,3023],{"class":300},"() ?: ",[253,3025,2285],{"class":263},[253,3027,512],{"class":300},[253,3029,3030],{"class":263},"IdMalformed",[253,3032,3033],{"class":300},"(present))\n",[253,3035,3036,3039],{"class":255,"line":304},[253,3037,3038],{"class":263},"            NoteId",[253,3040,3041],{"class":300},"(parsed)\n",[253,3043,3044],{"class":255,"line":325},[253,3045,697],{"class":300},[253,3047,3048],{"class":255,"line":340},[253,3049,548],{"class":300},[253,3051,3052],{"class":255,"line":353},[253,3053,413],{"class":300},[214,3055,3056,3057,3060,3061,3064,3065,3068,3069,3071,3072,3075,3076,3078],{},"The ",[250,3058,3059],{},"invoke"," operator makes ",[250,3062,3063],{},"NoteId(\"42\")"," return ",[250,3066,3067],{},"Either\u003CApiError, NoteId>",". Inside a route's ",[250,3070,1810],{}," block,\n",[250,3073,3074],{},"NoteId(call.parameters[\"id\"]).bind()"," either yields a valid ",[250,3077,2970],{}," or short-circuits the whole handler with the\nappropriate error — no try-catch, no manual null check.",[214,3080,3081],{},"You can also use the raise context and avoid the either block which works nicely in some simple cases. For example it's\nvery common to have a single string parameter expected on a request:",[243,3083,3085],{"className":245,"code":3084,"language":247,"meta":248,"style":248},"data class Tag(\n    val value: String,\n) {\n    companion object {\n        context(_: Raise\u003CApiError>)\n        operator fun invoke(raw: String?): Tag =\n            Tag(ensureNotNull(raw?.takeIf { it.isNotBlank() }) { TagNameMissing })\n    }\n}\n",[250,3086,3087,3098,3110,3114,3122,3137,3156,3176,3180],{"__ignoreMap":248},[253,3088,3089,3091,3093,3096],{"class":255,"line":256},[253,3090,291],{"class":259},[253,3092,294],{"class":259},[253,3094,3095],{"class":263}," Tag",[253,3097,301],{"class":300},[253,3099,3100,3102,3104,3106,3108],{"class":255,"line":267},[253,3101,328],{"class":259},[253,3103,1189],{"class":259},[253,3105,789],{"class":300},[253,3107,348],{"class":263},[253,3109,337],{"class":300},[253,3111,3112],{"class":255,"line":275},[253,3113,494],{"class":300},[253,3115,3116,3118,3120],{"class":255,"line":282},[253,3117,2938],{"class":259},[253,3119,1065],{"class":259},[253,3121,396],{"class":300},[253,3123,3124,3127,3129,3131,3133,3135],{"class":255,"line":288},[253,3125,3126],{"class":263},"        context",[253,3128,1914],{"class":300},[253,3130,1917],{"class":263},[253,3132,465],{"class":300},[253,3134,556],{"class":263},[253,3136,1924],{"class":300},[253,3138,3139,3141,3143,3145,3147,3149,3151,3154],{"class":255,"line":304},[253,3140,2947],{"class":259},[253,3142,708],{"class":259},[253,3144,2952],{"class":263},[253,3146,2955],{"class":300},[253,3148,348],{"class":263},[253,3150,2092],{"class":300},[253,3152,3153],{"class":263},"Tag",[253,3155,725],{"class":259},[253,3157,3158,3161,3163,3165,3167,3169,3171,3173],{"class":255,"line":325},[253,3159,3160],{"class":263},"            Tag",[253,3162,512],{"class":300},[253,3164,2209],{"class":263},[253,3166,2994],{"class":300},[253,3168,2997],{"class":263},[253,3170,3000],{"class":300},[253,3172,2023],{"class":263},[253,3174,3175],{"class":300},"() }) { TagNameMissing })\n",[253,3177,3178],{"class":255,"line":340},[253,3179,548],{"class":300},[253,3181,3182],{"class":255,"line":353},[253,3183,413],{"class":300},[214,3185,3186,3187,3189],{},"Usage of ",[250,3188,3153],{}," further in the call stack can then rely on the value being present.",[235,3191,3193],{"id":3192},"buildinfo-and-the-version-endpoint","BuildInfo and the Version Endpoint",[214,3195,3196,3197,3200,3201,3204],{},"Most of my services expose a ",[250,3198,3199],{},"\u002Fversion"," endpoint that returns the deployed image tag. The tag is written to a file on\nthe classpath during CI, and ",[250,3202,3203],{},"BuildInfo"," reads it at runtime:",[243,3206,3208],{"className":245,"code":3207,"language":247,"meta":248,"style":248},"import arrow.core.raise.Raise\nimport arrow.core.raise.catch\nimport arrow.core.raise.withError\n\nobject BuildInfo {\n    context(_: Raise\u003CApiError>)\n    fun imageTag(): String =\n        withError(::VersionNotReadable) {\n            catch({\n                BuildInfo::class.java\n                    .getResourceAsStream(\"\u002Fimage-tag.txt\")\n                    ?.use { it.readBytes().decodeToString().trim() }\n                    ?: \"development\"\n            }) { raise(it) }\n        }\n}\n",[250,3209,3210,3216,3222,3228,3232,3241,3255,3269,3282,3289,3299,3314,3341,3349,3358,3362],{"__ignoreMap":248},[253,3211,3212,3214],{"class":255,"line":256},[253,3213,260],{"class":259},[253,3215,1862],{"class":263},[253,3217,3218,3220],{"class":255,"line":267},[253,3219,260],{"class":259},[253,3221,2373],{"class":263},[253,3223,3224,3226],{"class":255,"line":275},[253,3225,260],{"class":259},[253,3227,2504],{"class":263},[253,3229,3230],{"class":255,"line":282},[253,3231,279],{"emptyLinePlaceholder":278},[253,3233,3234,3236,3239],{"class":255,"line":288},[253,3235,618],{"class":259},[253,3237,3238],{"class":263}," BuildInfo",[253,3240,396],{"class":300},[253,3242,3243,3245,3247,3249,3251,3253],{"class":255,"line":304},[253,3244,1911],{"class":263},[253,3246,1914],{"class":300},[253,3248,1917],{"class":263},[253,3250,465],{"class":300},[253,3252,556],{"class":263},[253,3254,1924],{"class":300},[253,3256,3257,3260,3263,3265,3267],{"class":255,"line":325},[253,3258,3259],{"class":259},"    fun",[253,3261,3262],{"class":263}," imageTag",[253,3264,459],{"class":300},[253,3266,348],{"class":263},[253,3268,725],{"class":259},[253,3270,3271,3274,3277,3280],{"class":255,"line":340},[253,3272,3273],{"class":263},"        withError",[253,3275,3276],{"class":300},"(::",[253,3278,3279],{"class":263},"VersionNotReadable",[253,3281,494],{"class":300},[253,3283,3284,3287],{"class":255,"line":353},[253,3285,3286],{"class":259},"            catch",[253,3288,2422],{"class":300},[253,3290,3291,3294,3296],{"class":255,"line":374},[253,3292,3293],{"class":300},"                BuildInfo::",[253,3295,319],{"class":263},[253,3297,3298],{"class":300},".java\n",[253,3300,3301,3304,3307,3309,3312],{"class":255,"line":379},[253,3302,3303],{"class":300},"                    .",[253,3305,3306],{"class":263},"getResourceAsStream",[253,3308,512],{"class":300},[253,3310,3311],{"class":515},"\"\u002Fimage-tag.txt\"",[253,3313,322],{"class":300},[253,3315,3316,3319,3322,3324,3327,3330,3333,3335,3338],{"class":255,"line":384},[253,3317,3318],{"class":300},"                    ?.",[253,3320,3321],{"class":263},"use",[253,3323,3000],{"class":300},[253,3325,3326],{"class":263},"readBytes",[253,3328,3329],{"class":300},"().",[253,3331,3332],{"class":263},"decodeToString",[253,3334,3329],{"class":300},[253,3336,3337],{"class":263},"trim",[253,3339,3340],{"class":300},"() }\n",[253,3342,3343,3346],{"class":255,"line":399},[253,3344,3345],{"class":300},"                    ?: ",[253,3347,3348],{"class":515},"\"development\"\n",[253,3350,3351,3354,3356],{"class":255,"line":410},[253,3352,3353],{"class":300},"            }) { ",[253,3355,2285],{"class":263},[253,3357,1548],{"class":300},[253,3359,3360],{"class":255,"line":416},[253,3361,697],{"class":300},[253,3363,3364],{"class":255,"line":421},[253,3365,413],{"class":300},[214,3367,3368,3369,3372,3373,3375],{},"When the file is absent (local development) it falls back to ",[250,3370,3371],{},"\"development\"",". When the file is present but unreadable\nfor some reason, ",[250,3374,3279],{}," propagates as a 500. The GitHub Actions step that writes the file goes before the\nbuild step:",[243,3377,3381],{"className":3378,"code":3379,"language":3380,"meta":248,"style":248},"language-yaml shiki shiki-themes github-dark","- name: Generate build info resource\n  run: |\n    mkdir -p src\u002Fmain\u002Fresources\n    echo \"sha-${GITHUB_SHA::7}\" > src\u002Fmain\u002Fresources\u002Fimage-tag.txt\n","yaml",[250,3382,3383,3397,3407,3412],{"__ignoreMap":248},[253,3384,3385,3388,3392,3394],{"class":255,"line":256},[253,3386,3387],{"class":300},"- ",[253,3389,3391],{"class":3390},"s4JwU","name",[253,3393,789],{"class":300},[253,3395,3396],{"class":515},"Generate build info resource\n",[253,3398,3399,3402,3404],{"class":255,"line":267},[253,3400,3401],{"class":3390},"  run",[253,3403,789],{"class":300},[253,3405,3406],{"class":259},"|\n",[253,3408,3409],{"class":255,"line":275},[253,3410,3411],{"class":515},"    mkdir -p src\u002Fmain\u002Fresources\n",[253,3413,3414],{"class":255,"line":282},[253,3415,3416],{"class":515},"    echo \"sha-${GITHUB_SHA::7}\" > src\u002Fmain\u002Fresources\u002Fimage-tag.txt\n",[214,3418,3419],{},"The route is one line:",[243,3421,3423],{"className":245,"code":3422,"language":247,"meta":248,"style":248},"get(\"\u002Fversion\") {\n    either { BuildInfo.imageTag() }.respond()\n}\n",[250,3424,3425,3436,3454],{"__ignoreMap":248},[253,3426,3427,3429,3431,3434],{"class":255,"line":256},[253,3428,2430],{"class":259},[253,3430,512],{"class":300},[253,3432,3433],{"class":515},"\"\u002Fversion\"",[253,3435,494],{"class":300},[253,3437,3438,3441,3444,3447,3450,3452],{"class":255,"line":267},[253,3439,3440],{"class":263},"    either",[253,3442,3443],{"class":300}," { BuildInfo.",[253,3445,3446],{"class":263},"imageTag",[253,3448,3449],{"class":300},"() }.",[253,3451,1516],{"class":263},[253,3453,2119],{"class":300},[253,3455,3456],{"class":255,"line":275},[253,3457,413],{"class":300},[235,3459,3461],{"id":3460},"putting-it-together","Putting It Together",[214,3463,3464,3465,3468],{},"To illustrate the full flow, here's how a single ",[250,3466,3467],{},"GET \u002Fapi\u002Fnotes\u002F{id}"," request travels through the layers:",[3470,3471,3472,3490,3505,3513],"ol",{},[3473,3474,3475,3476,3478,3479,3482,3483,3485,3486,3489],"li",{},"The route calls ",[250,3477,3074],{},". If the parameter is missing or non-numeric, an ",[250,3480,3481],{},"IdRequired","\nor ",[250,3484,3030],{}," error short-circuits the ",[250,3487,3488],{},"either"," block immediately.",[3473,3491,3492,3493,3496,3497,3499,3500,3502,3503,233],{},"If the ID is valid, ",[250,3494,3495],{},"service.getNote(id.value)"," is called inside the same ",[250,3498,1917],{}," context. If the repository returns\nnull, ",[250,3501,2209],{}," raises ",[250,3504,1966],{},[3473,3506,3507,3509,3510,233],{},[250,3508,1810],{}," captures whichever path was taken and produces an ",[250,3511,3512],{},"Either\u003CApiError, Note>",[3473,3514,3515,3517],{},[250,3516,1806],{}," sends either a 404 JSON body or a 200 JSON body. The route handler is finished.",[214,3519,3520,3521,3523],{},"No exceptions propagate. No ",[250,3522,1396],{}," branches are needed at the call site to figure out which HTTP status to return. Each\nerror type knows its own status code, and the sealed interface ensures nothing is forgotten.",[235,3525,3527],{"id":3526},"why-bother","Why Bother?",[214,3529,3530,3531,3533,3534,3537,3538,3541],{},"The pattern adds a small amount of overhead — the ",[250,3532,556],{}," hierarchy, the ",[250,3535,3536],{},"Respond.kt"," file, the\n",[250,3539,3540],{},"HttpStatusCodeSerializer"," — but each of those is written once per project and then mostly forgotten. What you get in\nreturn is:",[3543,3544,3545,3552,3562,3571,3577],"ul",{},[3473,3546,3547,3551],{},[3548,3549,3550],"strong",{},"Exhaustive errors"," — the compiler tells you when a new error type isn't handled somewhere it needs to be.",[3473,3553,3554,3557,3558,3561],{},[3548,3555,3556],{},"Clean routes"," — handlers are just ",[250,3559,3560],{},"either { }.respond()"," with business logic in between.",[3473,3563,3564,3567,3568,3570],{},[3548,3565,3566],{},"No hidden control flow"," — there's no question about which exceptions a service method can produce; the\n",[250,3569,2337],{}," context makes it explicit.",[3473,3572,3573,3576],{},[3548,3574,3575],{},"Consistent HTTP responses"," — every error goes through the same serialization path, so the JSON shape is always\npredictable.",[3473,3578,3579,3582],{},[3548,3580,3581],{},"Useful errors"," - since the raise mechanism short circuits early - you get the error that actually happened, not a\ndownstream symptom of it. For example - if an ID is malformed, you get that error instead of a 404 from the database\ndriver.",[3584,3585,3586],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}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 pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}",{"title":248,"searchDepth":267,"depth":267,"links":3588},[3589,3593,3594,3598,3599,3600,3601],{"id":237,"depth":267,"text":238,"children":3590},[3591,3592],{"id":856,"depth":275,"text":857},{"id":1043,"depth":275,"text":1044},{"id":1403,"depth":267,"text":1404},{"id":1824,"depth":267,"text":1825,"children":3595},[3596,3597],{"id":2221,"depth":275,"text":2222},{"id":2341,"depth":275,"text":2342},{"id":2622,"depth":267,"text":2623},{"id":3192,"depth":267,"text":3193},{"id":3460,"depth":267,"text":3461},{"id":3526,"depth":267,"text":3527},"2026-05-07 10:18 +0200","I use Ktor for a lot of projects - it's lightweight and simple to use. However - when providing any\nsort of API I want to make sure that the errors are actually useful and as specific as possible. For that I like to\nuse Arrow.",null,"md","Using Arrow in Ktor for controlled error handling. Sealed interfaces for error types, Raise for propagation, and extension functions for HTTP responses.",{},"\u002F2026\u002F05\u002F07\u002Ffunctional-error-handling-in-ktor-with-arrow",{"title":209,"description":3603},{"loc":3608},"2026\u002F05\u002F07\u002Ffunctional-error-handling-in-ktor-with-arrow",[247,3613,3614],"ktor","arrow","Eyo62KZChp8N7ICOGAJn9uk6tiyhXK7TX4Z7LpyQRw4",1778149363757]