The Bare Except That Swallows Everything
TL;DR: Bare except clauses catch everything including KeyboardInterrupt, making code impossible to debug or stop.
The Code
1def process_user_data(user_id):
2 try:
3 user = database.get_user(user_id)
4 user.update_last_login()
5 send_welcome_email(user.email)
6 update_analytics(user)
7 return user
8 except:
9 return None
10The Prayer 🙏
"Surely nothing will go wrong inside this try block! And if something does go wrong, well... we'll just return None and pretend it never happened. The calling code will figure it out. Right?"
The Reality Check
This bare except: clause catches everything. And when we say everything, we mean EVERYTHING:
KeyboardInterruptwhen you try to stop the program? Caught.SystemExitwhen you try to exit gracefully? Caught.MemoryErrorwhen you're out of RAM? Caught and ignored.- Typo in variable name (
user.emalinstead ofuser.email)? Silent failure. - Database connection lost? No one will ever know.
Now imagine this code running in production:
- Database goes down temporarily
- Function silently returns
None - Calling code assumes user doesn't exist
- Creates duplicate user account
- Database comes back up
- Now you have two users with the same email
- Customer can't log in
- Support ticket filed
- 3 hours of debugging later, you find this
except:block
The real kicker? When you try to debug this with print statements or a debugger, even KeyboardInterrupt (Ctrl+C) gets swallowed. You can't even stop the program gracefully!
The Fix
Option 1: Catch Specific Exceptions (Best Practice)
1def process_user_data(user_id):
2 try:
3 user = database.get_user(user_id)
4 user.update_last_login()
5 send_welcome_email(user.email)
6 update_analytics(user)
7 return user
8 except database.UserNotFound:
9 logger.warning(f"User {user_id} not found")
10 return None
11 except database.ConnectionError as e:
12 logger.error(f"Database connection failed: {e}")
13 raise # Re-raise to let caller handle it
14 except EmailDeliveryError as e:
15 # Email failed, but user update succeeded - that's okay
16 logger.warning(f"Failed to send welcome email: {e}")
17 return user
18Now you know exactly what can go wrong and handle each case appropriately.
Option 2: Catch Exception (If You Must)
1def process_user_data(user_id):
2 try:
3 user = database.get_user(user_id)
4 user.update_last_login()
5 send_welcome_email(user.email)
6 update_analytics(user)
7 return user
8 except Exception as e:
9 # At least log what went wrong!
10 logger.error(f"Failed to process user {user_id}: {e}", exc_info=True)
11 return None
12Using except Exception: is better than bare except: because it won't catch KeyboardInterrupt, SystemExit, or GeneratorExit - things you almost never want to suppress.
Option 3: Let It Fail (Often the Right Choice)
1def process_user_data(user_id):
2 # No try/except at all!
3 user = database.get_user(user_id)
4 user.update_last_login()
5 send_welcome_email(user.email)
6 update_analytics(user)
7 return user
8Sometimes the best error handling is to let exceptions propagate. The calling code or a framework-level handler can deal with it appropriately.
Lesson Learned
Bare except: clauses catch everything, including KeyboardInterrupt and SystemExit, making your code impossible to debug or stop. Always catch specific exceptions, or at minimum use except Exception: with proper logging.