Ett sväljt undantag kan radera bevis i stället för att hantera felet. Ett except- eller catch-block är motiverat när felet är känt, kan hanteras just här och blocket bevarar en avsiktlig signal eller åtgärd genom loggning, omförsök eller vidarekastning. I Python betyder känt fel normalt en specifik undantagstyp; i JavaScript och TypeScript, där typen inte anges i catch-satsen, behöver try-blocket vara snävt och oväntade fel kastas vidare. Det finns ett smalt undantag: ett specifikt fel får ignoreras tyst när fortsatt körning uttryckligen är rätt beteende och ett test bevakar det. I övriga fall ska felet propagera. Att hitta blocken är sökarbete med regex; att avgöra vilka som får stanna kräver den här kontraktsbedömningen.
Samma fel i fyra skepnader
Mönstret ser olika ut i olika språk, men gör samma sak: det byter ett fel mot ett värde som ser normalt ut. Ett vanligt Python-mönster är en tom retur som passar in i en typsignatur som säger Optional:
def hamta_belopp(rad):
try:
return int(rad["belopp"])
except Exception:
return None
JavaScript-varianten behöver inte ens en retur. Ett tomt catch räcker, och blocket ser ut som en medveten markering av att felet är ointressant:
try {
await skickaKvitto(order);
} catch (e) {
// bara ett kvitto, inte kritiskt
}
I Go finns ingen undantagsmekanism, men ett returnerat fel kan tilldelas _, eller bara loggas på debug-nivå innan funktionen fortsätter. Anroparen kan då få samma normala resultat som om inget fel inträffat; om ett användbart spår finns kvar beror på loggkonfigurationen. I Java är den klassiska formen ett catch (Exception e) som anropar e.printStackTrace() och sedan låter metoden returnera som vanligt: spåret hamnar på standard error i en container ingen läser, medan anroparen får ett svar som betyder att allt gick bra.
MITRE har en egen post för precis det här. CWE-390, Detection of Error Condition Without Action, beskriver att produkten upptäcker ett specifikt fel men inte vidtar några åtgärder för att hantera det, och listar konsekvensen som oväntat tillstånd och förändrad exekveringslogik [1]. Den snävare CWE-1069, Empty Exception Block, gäller ett felhanteringsblock som inte innehåller någon kod alls [2]. Numren skiljer det allmänna passiva felblocket från det helt tomma; de säger inte hur vanligt mönstret är.
Risken blir särskilt värd att granska när instruktionen belönar kod som är "robust", "inte kraschar" eller "hanterar fel" utan att samtidigt ange vilket felkontrakt som ska gälla. Då kan en modell välja en normal retur i stället för att bevara felsignalen. Det är en felhypotes att testa, inte ett påstående om att alla modeller gör så. Pythons egen dokumentation rekommenderar att vara så specifik som möjligt med vilka undantagstyper man tänker hantera och att låta oväntade undantag propagera vidare [3].
Varför testsviten är grön ändå
Ett sväljt undantag gör två saker samtidigt, och det är den andra som är dyr. Den första är att felet försvinner ur loggen. Den andra är att funktionens kontrakt tyst byts ut: den lovade ett belopp eller ett fel, och levererar nu ett belopp eller None. Om anropande kod filtrerar bort tomma värden förvandlas felet till dataförlust.
Så här ser det ut mätt. Funktionen ovan kördes mot 1 000 rader där 47 var trasiga, antingen med tomt beloppsfält eller med fältet helt saknat. Loggningen stod på DEBUG, alltså den lägsta nivån, så ingenting var bortfiltrerat:
rader in: 1000 rader ut: 953 tappade: 47
summa: 476290
Fyrtiosju rader lämnade aldrig ett spår. Inte en rad i loggen, inget felmeddelande, och processen avslutades med exitkod 0. En avstämning som jämför summan mot källsystemet hittar felet — om någon gör en avstämning. Annars är den enda kvarvarande signalen att en siffra är för låg.
Testerna kan vara gröna av samma skäl. Ett test som utgår från samma otydliga krav som funktionen kan kontrollera att den inte kastar, eller att den returnerar None för trasig indata. Båda är sanna. Det är inte ett dåligt test i sig — det är ett test av fel kontrakt, och skillnaden syns först när man skriver ett test som kräver motsatsen.
Det här är också förklaringen till en obehaglig upplevelse längre fram: att felsöka något som inte lämnat några spår. Navsidan om felsökning börjar där felet är känt och ska förstås. Ett sväljt undantag ligger steget före — det gör att felsökningen aldrig kan börja, för det finns inget att läsa. När problemet upptäcks i drift blir jobbet i stället en rekonstruktion i efterhand, vilket är precis den situation AI-agenten som gjorde fel i produktion handlar om.
Sök fram dem: tre mönster och tre lintregler
Blocken går att hitta mekaniskt. Tre sökningar täcker de vanligaste formerna. Med ripgrep, där -U tillåter träffar över radbrytning och --pcre2 ger fullt regexstöd:
rg -U --pcre2 -g '*.py' 'except[^\n]*:\s*\n\s*pass\s*\n'
rg -U --pcre2 -g '*.py' 'except[^\n]*:\s*\n\s*return(\s+None)?\s*\n'
rg -U --pcre2 -g '*.py' 'except[^\n]*:\s*\n\s*continue\s*\n'
För JavaScript och TypeScript är motsvarigheten ett catch vars kropp är tom eller bara innehåller en kommentar:
rg -U --pcre2 -g '*.{js,ts,jsx,tsx}' 'catch\s*\([^)]*\)\s*\{\s*\}'
rg -U --pcre2 -g '*.{js,ts,jsx,tsx}' 'catch\s*\([^)]*\)\s*\{\s*(//[^\n]*\n\s*)+\}'
Räkna med brus, och räkna med mycket brus. Samma tre Python-sökningar kördes mot standardbiblioteket i Python 3.14.2 — 654 .py-filer, med site-packages och testpaketen bortsorterade — och gav 483 träffar på pass, 91 på return och 47 på continue, plus 154 fall av bara except: utan typ. Motsvarande sökning i 5 097 icke-minifierade .js-filer i ett installerat beroendeträd gav 81 tomma catch-block och 17 som bara innehöll en kommentar. Poängen med siffrorna är inte att standardbiblioteket är dåligt skrivet — många av träffarna är legitima — utan att en träff är en fråga, aldrig en dom. Kör sökningen på din diff först, inte på hela kodbasen, så blir mängden hanterbar.
Lintverktygen tar den återkommande delen. Tre regler räcker långt, och de gör tre olika saker:
E722 — bare-except. Fångar except: utan typ. Motiveringen i Ruffs dokumentation är att ett naket except fångar BaseException, vilket inkluderar KeyboardInterrupt och SystemExit, och att det både kan göra programmet svårt att avbryta och dölja andra problem [4].BLE001 — blind-except. Ett steg strängare: den träffar except-satser som fångar alla undantag, inklusive både except BaseException och except Exception [5]. Den fångar alltså det breda Exception-mönstret i exemplet ovan, medan E722 inte gör det.S110 — try-except-pass. Träffar själva sväljningen oavsett vilken typ som fångas. Regelns rekommendation är att logga undantaget i stället för att ignorera det, och exempelkoden i dokumentationen använder logging.exception [6].I JavaScript-världen är motsvarigheten ESLint-regeln no-empty, som förbjuder tomma block. Läs villkoren noga innan du litar på den: regeln ignorerar block som innehåller en kommentar, och den har dessutom en option allowEmptyCatch som tillåter tomma catch-satser helt [7]. Ett projekt som satt den flaggan har en lintregel som ser aktiv ut i konfigurationen men inte fångar mönstret alls.
Just undantaget för kommentarer gör varianten osynlig för verktyget. catch (e) { /* ignoreras avsiktligt */ } passerar no-empty i standarduppsättning. Kommentaren kan dokumentera ett legitimt beslut, men den skapar varken observabilitet eller en spärr mot att andra fel tystas. I JavaScript och TypeScript bör blocket därför skilja det förväntade felet från oväntade fel och kasta de senare vidare. Om fortsatt tyst körning verkligen är kontraktet ska ett test bevisa just det.
De tre villkoren för att få fånga ett undantag
Sökningen ger en lista. Listan behöver ett beslut per rad. De tre villkoren nedan ska normalt hålla samtidigt; avsteg för medveten tyst undertryckning kräver ett specifikt fel, ett uttryckligt kontrakt och ett test.
FileNotFoundError, JSONDecodeError, TimeoutError. Ett except Exception kräver en tydlig gräns, till exempel ett toppnivåblock som loggar och kastar vidare; det ska inte användas för att välja en normal retur när felet är okänt. Pythons standardbibliotek har ett färdigt uttryck för det snäva fallet: contextlib.suppress returnerar en kontexthanterare som undertrycker de angivna undantagen, med den uttryckliga reservationen i dokumentationen att den, som varje mekanism som helt undertrycker undantag, bara ska användas för mycket specifika fel där det är känt att det är rätt att fortsätta tyst [8].try-blocket och contextlib.suppress avbryter hela blocket vid felet, så koden efter den felande raden inne i blocket körs aldrig [8]. Ett brett try runt fem satser är därför inte bara för brett i typ, utan också i omfång.logging.exception på nivån ERROR med undantagsinformationen tillagd, och dokumentationen anger att den bara ska anropas från en undantagshanterare [9]. Ett logging.error(str(e)) är inte samma sak — det ger felmeddelandet utan stacktrace, alltså raden men inte vägen dit. Vill du kasta vidare något annat: använd raise NyttFel(...) from exc, så att kedjan pekar ut det ursprungliga felet i stället för att dölja det [3].Villkor tre är också gränsen mot ett angränsande ämne. Ett omförsök är en giltig åtgärd, men bara för fel som är tillfälliga — en timeout, en 503, en avbruten anslutning. Vilka fel som får försökas igen, och hur backoff och idempotens hänger ihop med det, hör till API-integrationer med AI och upprepas inte här. Det som gäller i det här sammanhanget är enklare: ett omförsök som ger upp tyst är fortfarande ett sväljt fel.
Testet som failar när felet tystas
Ett fel som ska propagera är ett kontrakt, och kontrakt kan testas. Testet nedan använder bara standardbiblioteket och kör som det står. Poängen med formen är att exakt samma två testfall körs mot båda varianterna av funktionen, så att skillnaden inte kan bortförklaras med att testerna är olika:
import unittest
class Konto:
def __init__(self): self.rader = []
def bokfor(self, belopp):
if belopp < 0:
raise ValueError("negativt belopp")
self.rader.append(belopp)
def svalt(konto, belopp): # monstret modellen skriver
try:
konto.bokfor(belopp)
except Exception:
return None
return True
def propagerande(konto, belopp): # felet far ga vidare
konto.bokfor(belopp)
return True
class Kontrakt:
funktion = None
def test_felet_bubblar_upp(self):
with self.assertRaises(ValueError):
self.funktion(Konto(), -5)
def test_ingen_rad_bokfordes(self):
konto = Konto()
try:
self.funktion(konto, -5)
except ValueError:
pass
self.assertEqual(konto.rader, [])
class TestSvalt(Kontrakt, unittest.TestCase):
funktion = staticmethod(svalt)
class TestPropagerande(Kontrakt, unittest.TestCase):
funktion = staticmethod(propagerande)
if __name__ == "__main__":
unittest.main(verbosity=2)
Körd på Python 3.14.2 under Windows 11 ger den fyra testfall, ett misslyckande och exitkod 1:
test_felet_bubblar_upp (TestPropagerande...) ... ok
test_ingen_rad_bokfordes (TestPropagerande...) ... ok
test_felet_bubblar_upp (TestSvalt...) ... FAIL
test_ingen_rad_bokfordes (TestSvalt...) ... ok
AssertionError: ValueError not raised
Ran 4 tests in 0.001s
FAILED (failures=1)
Det intressanta är inte att ett test failar, utan vilket. Kontrollen att ingen rad bokfördes passerar för båda varianterna — den sväljande funktionen skrev ju heller ingen rad. Ett test som bara kontrollerar att biverkan uteblev bevakar alltså ingenting alls här. Det är bara assertRaises som skiljer en funktion som stoppade felet från en som gömde det.
Använder du pytest är motsvarigheten pytest.raises som kontexthanterare, med match= för att dessutom kräva att felmeddelandet stämmer; match jämförs med re.search(), så en delsträng räcker [10]. Att kräva rätt meddelande är värt besväret när koden kastar samma undantagstyp av flera olika skäl.
Kör kontraktstestet mot det sväljande blocket innan du rättar det, och se det falla med rätt felmeddelande. Ser du testet bara passera efter åtgärden vet du inte om det hade fångat den ursprungliga buggen. Avvägningen är att ett kontraktstest per felväg kostar rader i testsviten: skriv det för de fel som får konsekvenser i data eller pengar, inte för varenda try i kodbasen.
Fem frågor att ställa till diffen
Felhantering är en av de sju frågorna per diff i guiden om kodgranskning. De fem frågorna här är fördjupningen bakom just den frågan, och de går att ställa utan att köra något.
Exception, BaseException eller inget alls krävs en uttrycklig motivering till varför blocket ligger vid en gräns och hur oväntade fel bevaras. Fråga vilket konkret fel blocket finns för.None, tom lista eller false betyder att felet blivit ett giltigt värde. Följ värdet till anroparen och se vad som händer med det där.logging.exception eller logger.error(..., exc_info=True) ger vägen; logging.error(str(e)) ger bara raden. Kontrollera också att den valda nivån faktiskt samlas in och går att larma på i drift; en DEBUG-rad är värdelös om driftmiljön filtrerar bort den.try-blocket? Varje extra sats breddar mängden fel som blocket kan fånga. Kontrollera att omfånget inte täcker operationer vars fel ska hanteras på ett annat sätt — och kom ihåg att satser efter den felande raden i blocket aldrig körs.
[1] MITRE, CWE-390: Detection of Error Condition Without Action — produkten upptäcker ett specifikt fel men vidtar inga åtgärder för att hantera det.
[2] MITRE, CWE-1069: Empty Exception Block — ett felhanteringsblock som inte innehåller någon kod.
[3] Python-dokumentationen, Errors and Exceptions — god sed att vara så specifik som möjligt med hanterade undantagstyper och låta oväntade undantag propagera; raise ... from för att markera att ett fel är en direkt följd av ett annat.
[4] Ruff, E722 bare-except — ett naket except fångar BaseException och kan dölja andra problem.
[5] Ruff, BLE001 blind-except — träffar except-satser som fångar alla undantag, inklusive except Exception.
[6] Ruff, S110 try-except-pass — mönstret undertrycker alla undantag; rekommendationen är att logga i stället för att ignorera.
[7] ESLint, no-empty — förbjuder tomma block, ignorerar block som innehåller en kommentar och har optionen allowEmptyCatch.
[8] Python-dokumentationen, contextlib.suppress — undertrycker angivna undantag och återupptar körningen efter with-satsen; ska bara användas för mycket specifika fel där det är känt att det är rätt att fortsätta tyst.
[9] Python-dokumentationen, logging.exception — loggar på nivån ERROR med undantagsinformation tillagd och ska bara anropas från en undantagshanterare.
[10] pytest, Assertions about expected exceptions — pytest.raises som kontexthanterare och match= som jämförs med re.search().
Samtliga källor lästa 20 augusti 2026. Mätvärdena i texten kommer från körningar under arbetet med artikeln på Python 3.14.2 under Windows 11, med ripgrep 14.1.1 för sökmönstren. De är exempel på storleksordning i den miljön, inte allmängiltiga tal.