Lesson 16: Spring Boot Exception Handling
Master exception handling in Spring Boot: global error handling, custom exceptions, validation, and building robust applications that gracefully handle errors.
Introduction
Building robust applications means preparing for things to go wrong - user input errors, database connection failures, missing resources, and unexpected system issues. Spring Boot provides powerful exception handling mechanisms that let you intercept errors and return meaningful responses to clients instead of cryptic stack traces. Think of exception handling as your application's emergency response system, like how hospitals have protocols for different medical emergencies. When something goes wrong, you want clear, helpful responses that guide users toward solutions rather than confusing technical details. This lesson teaches you to build comprehensive error handling that makes your applications more user-friendly, easier to debug, and more professional in how they communicate problems to both users and developers.
Understanding Exceptions
Definition
Exceptions in Spring Boot are events that disrupt the normal flow of your application, such as invalid user input, database errors, or missing resources. By default, Spring Boot returns generic error pages with status codes like 404 or 500, but proper exception handling allows you to customize these responses with meaningful messages and appropriate HTTP status codes. Understanding different exception types helps you handle various error scenarios appropriately - from client errors like validation failures to server errors like database connection issues.
Analogy
Think of exceptions like different types of emergencies in a well-organized hotel. When a guest requests a room that doesn't exist (404 error), the front desk doesn't just say "computer says no" - they explain that the room number is invalid and offer alternatives. When the elevator breaks down (500 error), staff don't abandon guests between floors - they have backup procedures, apologize for the inconvenience, and guide guests to working elevators. Similarly, good exception handling in your application means intercepting these "emergency situations" and responding with helpful, professional messages that guide users toward solutions rather than displaying confusing technical error details that only developers understand.
Examples
Common Spring Boot exceptions:
// Resource not found
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "User not found");
// Bad request
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid email format");
// Internal server error
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Database connection failed");
Exception without handling (default behavior):
// Default Spring Boot error response
{
"timestamp": "2023-10-15T10:30:00.000+00:00",
"status": 404,
"error": "Not Found",
"path": "/api/users/999"
}
Built-in Spring Boot exceptions:
// These are automatically handled by Spring Boot
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException("User not found")); // 404
}
Global Exception Handling
Definition
Global exception handling using @ControllerAdvice allows you to centralize error handling logic across your entire application instead of repeating try-catch blocks in every controller. This annotation creates a global exception handler that intercepts exceptions from any controller and provides consistent error responses. You can handle specific exception types differently and ensure all errors return properly formatted responses with appropriate HTTP status codes and meaningful messages.
Analogy
Global exception handling is like having a centralized customer service department in a large company instead of each department handling complaints individually. When a customer has a problem with billing, shipping, or technical support, they all get routed to the same professional customer service team that knows exactly how to handle different types of issues. The customer service team has standard procedures for each problem type - billing issues get one response template, shipping problems get another, and technical issues get escalated appropriately. This ensures every customer gets consistent, professional help regardless of which department caused the problem, and the company maintains its reputation for good customer service across all interactions.
Examples
Basic global exception handler:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity handleNotFound(EntityNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
}
}
Handling multiple exception types:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity handleBadRequest(IllegalArgumentException ex) {
ErrorResponse error = new ErrorResponse("Bad Request", ex.getMessage());
return ResponseEntity.badRequest().body(error);
}
@ExceptionHandler(DataAccessException.class)
public ResponseEntity handleDatabaseError(DataAccessException ex) {
ErrorResponse error = new ErrorResponse("Database Error", "Service temporarily unavailable");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}
Generic exception handler:
@ExceptionHandler(Exception.class)
public ResponseEntity handleGenericException(Exception ex) {
ErrorResponse error = new ErrorResponse("Internal Error", "An unexpected error occurred");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
Handler with logging:
@ExceptionHandler(Exception.class)
public ResponseEntity handleException(Exception ex) {
logger.error("Unhandled exception occurred", ex);
ErrorResponse error = new ErrorResponse("Error", "Something went wrong");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
Custom Exceptions
Definition
Custom exceptions allow you to create specific error types for your business logic that carry meaningful information about what went wrong. Instead of throwing generic exceptions, you can create domain-specific exceptions like UserNotFoundException, InsufficientFundsException, or DuplicateEmailException. Custom exceptions make your code more readable, easier to debug, and allow for more precise error handling where different exception types can be handled with different responses and status codes.
Analogy
Custom exceptions are like having specific emergency codes in a hospital instead of just saying "something's wrong." Instead of a generic "code blue" for every emergency, hospitals have "code red" for fire, "code silver" for weapons, "code pink" for infant abduction, and "code white" for pediatric emergency. Each code triggers a specific response protocol - the right people respond, they bring the right equipment, and they follow the right procedures. Similarly, custom exceptions like "UserNotFoundException" or "PaymentFailedException" tell your application exactly what type of problem occurred, so it can respond with the right error message, status code, and recovery actions rather than a generic "something went wrong" response.
Examples
Creating custom exception classes:
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}
Business-specific custom exceptions:
public class InsufficientFundsException extends RuntimeException {
private final BigDecimal balance;
private final BigDecimal requested;
public InsufficientFundsException(BigDecimal balance, BigDecimal requested) {
super(String.format("Insufficient funds: balance %.2f, requested %.2f", balance, requested));
this.balance = balance;
this.requested = requested;
}
}
Using custom exceptions in services:
@Service
public class UserService {
public User findUser(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User with ID " + id + " not found"));
}
}
Handling custom exceptions globally:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity handleUserNotFound(UserNotFoundException ex) {
ErrorResponse error = new ErrorResponse("User Not Found", ex.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(InsufficientFundsException.class)
public ResponseEntity handleInsufficientFunds(InsufficientFundsException ex) {
ErrorResponse error = new ErrorResponse("Payment Failed", ex.getMessage());
return ResponseEntity.status(HttpStatus.PAYMENT_REQUIRED).body(error);
}
}
Error Response Structure
Definition
A well-structured error response provides consistent, informative feedback to API clients by including essential information like error codes, human-readable messages, timestamps, and request paths. Good error responses help developers integrate with your API and help users understand what went wrong and how to fix it. The structure should be consistent across all error types so clients can reliably parse and handle errors programmatically.
Analogy
Error response structure is like a standardized incident report form that emergency services use. Whether it's a car accident, medical emergency, or building fire, the report always includes the same key information: what happened (error type), when it happened (timestamp), where it happened (request path), who was involved (user context), and what actions were taken (status code). This standardized format means that anyone reading the report - whether it's a police officer, insurance agent, or court official - knows exactly where to find the information they need. Similarly, a well-structured API error response gives client applications a predictable format they can rely on to extract the information needed to handle the error appropriately.
Examples
Basic error response class:
public class ErrorResponse {
private String error;
private String message;
private LocalDateTime timestamp;
private String path;
// constructors and getters
}
Detailed error response structure:
public class DetailedErrorResponse {
private String error;
private String message;
private LocalDateTime timestamp;
private String path;
private String requestId;
private List details;
public DetailedErrorResponse(String error, String message, String path) {
this.error = error;
this.message = message;
this.path = path;
this.timestamp = LocalDateTime.now();
this.requestId = UUID.randomUUID().toString();
}
}
Error response with validation details:
public class ValidationErrorResponse extends ErrorResponse {
private Map fieldErrors;
public ValidationErrorResponse(String message, Map fieldErrors) {
super("Validation Failed", message);
this.fieldErrors = fieldErrors;
}
}
Example JSON error response:
{
"error": "User Not Found",
"message": "User with ID 123 does not exist",
"timestamp": "2023-10-15T10:30:00",
"path": "/api/users/123",
"requestId": "abc-123-def"
}
Validation Handling
Definition
Validation handling captures and processes validation errors that occur when request data doesn't meet your defined constraints using annotations like @Valid, @NotNull, @Size, or @Email. Spring Boot automatically throws MethodArgumentNotValidException for request body validation failures and ConstraintViolationException for path variable or request parameter validation failures. Proper validation handling provides clear, field-specific error messages that help users correct their input.
Analogy
Validation handling is like a bouncer at an exclusive club who checks everyone at the door against a specific list of requirements. The bouncer doesn't just say "you can't come in" - they explain exactly what's wrong: "you need to be 21 or older," "you need proper attire," or "you need to be on the guest list." They might check multiple things at once and give you a complete list of everything you need to fix before you can enter. Similarly, validation handling in your API checks incoming data against your rules and provides specific, helpful error messages for each field that doesn't meet the requirements, so users know exactly what to correct in their requests.
Examples
Model with validation annotations:
public class User {
@NotBlank(message = "Name is required")
private String name;
@Email(message = "Email must be valid")
private String email;
@Min(value = 18, message = "Age must be at least 18")
private Integer age;
}
Controller with validation:
@PostMapping("/users")
public ResponseEntity createUser(@Valid @RequestBody User user) {
User savedUser = userService.save(user);
return ResponseEntity.ok(savedUser);
}
Handling validation exceptions:
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity handleValidation(MethodArgumentNotValidException ex) {
Map errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage()));
ValidationErrorResponse response = new ValidationErrorResponse("Validation failed", errors);
return ResponseEntity.badRequest().body(response);
}
Example validation error response:
{
"error": "Validation Failed",
"message": "Request contains invalid data",
"fieldErrors": {
"email": "Email must be valid",
"age": "Age must be at least 18"
}
}
HTTP Status Codes
Definition
HTTP status codes communicate the outcome of API requests using standardized numbers that clients can understand programmatically. Using appropriate status codes helps clients handle responses correctly - 200 for success, 400 for client errors, 404 for not found, 500 for server errors. Each status code has specific meaning and helps both developers and client applications understand whether the request succeeded, failed due to client issues, or failed due to server problems.
Analogy
HTTP status codes are like the standardized response signals that air traffic control uses with pilots. When a pilot requests permission to land, they don't get a long explanation - they get a clear, standardized response: "cleared to land" (200 OK), "runway occupied, circle and wait" (429 Too Many Requests), "airport closed" (503 Service Unavailable), or "invalid flight plan" (400 Bad Request). These codes are universally understood by all pilots and air traffic controllers worldwide, so there's no confusion about what action to take. Similarly, HTTP status codes provide a universal language that all web clients and servers understand, making it easy for applications to respond appropriately to different situations without needing to parse complex error messages.
Examples
Using appropriate status codes:
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity handleUserNotFound(UserNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse(ex.getMessage()));
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity handleBadRequest(IllegalArgumentException ex) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ErrorResponse(ex.getMessage()));
}
Custom status codes for business logic:
@ExceptionHandler(InsufficientFundsException.class)
public ResponseEntity handleInsufficientFunds(InsufficientFundsException ex) {
return ResponseEntity.status(HttpStatus.PAYMENT_REQUIRED).body(new ErrorResponse(ex.getMessage()));
}
@ExceptionHandler(DuplicateEmailException.class)
public ResponseEntity handleDuplicateEmail(DuplicateEmailException ex) {
return ResponseEntity.status(HttpStatus.CONFLICT).body(new ErrorResponse(ex.getMessage()));
}
Status code mapping examples:
// 200 OK - Success
return ResponseEntity.ok(user);
// 201 Created - Resource created
return ResponseEntity.status(HttpStatus.CREATED).body(newUser);
// 204 No Content - Success with no response body
return ResponseEntity.noContent().build();
Common status codes for exceptions:
// 400 Bad Request - Invalid input
// 401 Unauthorized - Authentication required
// 403 Forbidden - Access denied
// 404 Not Found - Resource doesn't exist
// 409 Conflict - Resource already exists
// 422 Unprocessable Entity - Validation failed
// 500 Internal Server Error - Unexpected server error
Exception Logging
Definition
Exception logging captures detailed information about errors for debugging and monitoring purposes while returning user-friendly messages to clients. Good logging includes the full stack trace, request context, user information, and timestamp, but client responses should only include appropriate information that helps users fix the problem. Different log levels (ERROR, WARN, INFO) help filter and prioritize log messages in production environments.
Analogy
Exception logging is like the detailed incident reports that security guards write versus the brief announcements they make over the intercom. When there's a problem in a shopping mall, the security guard might announce "Attention shoppers, elevator 2 is temporarily out of service" (user-friendly client message), but their incident report includes much more detail: exact time, witness statements, possible causes, photos of the scene, and action steps taken (detailed logs for internal use). The public gets just enough information to adjust their behavior, while management gets the full picture needed to understand what happened, prevent it from happening again, and improve their procedures.
Examples
Logging with different levels:
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity handleUserNotFound(UserNotFoundException ex) {
logger.warn("User not found: {}", ex.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("User not found", ex.getMessage()));
}
Detailed logging for server errors:
@ExceptionHandler(DataAccessException.class)
public ResponseEntity handleDatabaseError(DataAccessException ex) {
logger.error("Database error occurred", ex);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("Database Error", "Service temporarily unavailable"));
}
Structured logging with context:
@ExceptionHandler(Exception.class)
public ResponseEntity handleGeneral(Exception ex, HttpServletRequest request) {
String requestId = UUID.randomUUID().toString();
logger.error("Unhandled exception [requestId: {}, path: {}, method: {}]",
requestId, request.getRequestURI(), request.getMethod(), ex);
ErrorResponse error = new ErrorResponse("Internal Error", "An error occurred");
error.setRequestId(requestId);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
Security-conscious logging:
@ExceptionHandler(AuthenticationException.class)
public ResponseEntity handleAuth(AuthenticationException ex) {
// Log without sensitive details
logger.warn("Authentication failed for request: {}", ex.getMessage());
// Don't expose internal details to client
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(new ErrorResponse("Authentication Required", "Please provide valid credentials"));
}
Error Page Customization
Definition
Error page customization allows you to provide branded, user-friendly error pages for web applications instead of generic Spring Boot error pages. You can create custom error pages for different HTTP status codes, add helpful information and navigation, and maintain your application's look and feel even when errors occur. This improves user experience by providing consistent branding and helpful guidance when things go wrong.
Analogy
Error page customization is like having a professional, helpful receptionist handle problems in your office building instead of just posting generic "OUT OF ORDER" signs. When the elevator breaks down, instead of a basic sign that just says "broken," the receptionist puts up a professional notice that matches the building's branding, explains the situation clearly, provides alternative options (like directions to the stairs or other elevators), includes an estimated repair time, and offers to help with special accommodations. The message maintains the building's professional image while being genuinely helpful to visitors, rather than leaving them confused and frustrated by a bare-bones error message.
Examples
Custom error controller:
@Controller
public class CustomErrorController implements ErrorController {
@RequestMapping("/error")
public String handleError(HttpServletRequest request, Model model) {
Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
if (statusCode == 404) {
return "error/404";
} else if (statusCode == 500) {
return "error/500";
}
return "error/generic";
}
}
Error page with model data:
@RequestMapping("/error")
public String handleError(HttpServletRequest request, Model model) {
String errorMessage = (String) request.getAttribute(RequestDispatcher.ERROR_MESSAGE);
String requestUri = (String) request.getAttribute(RequestDispatcher.ERROR_REQUEST_URI);
model.addAttribute("errorMessage", errorMessage);
model.addAttribute("requestUri", requestUri);
return "error/custom";
}
Custom error page HTML (error/404.html):
<!DOCTYPE html>
<html>
<body>
<h1>Page Not Found</h1>
<p>The page you're looking for doesn't exist.</p>
<a href="/">Return to Home</a>
</body>
</html>
Status-specific error pages configuration:
# application.properties
server.error.whitelabel.enabled=false
server.error.include-message=always
server.error.include-binding-errors=always
Testing Exceptions
Definition
Testing exception handling ensures that your application responds correctly to error conditions by verifying that the right exceptions are thrown, appropriate HTTP status codes are returned, and error messages are formatted correctly. Good exception testing covers both the happy path and various error scenarios, ensuring your global exception handlers work as expected and that error responses match your specifications.
Analogy
Testing exception handling is like conducting fire drills and emergency simulations in a building to make sure everyone knows what to do when real emergencies occur. You don't wait for an actual fire to see if your evacuation procedures work - you regularly test the alarm systems, practice evacuation routes, and verify that emergency responders can access the building and communicate effectively. Similarly, you shouldn't wait for users to encounter errors to discover that your exception handling doesn't work properly. You systematically test different error scenarios to ensure your application responds appropriately, provides helpful messages, and maintains security even when things go wrong.
Examples
Testing exception responses:
@Test
void shouldReturn404WhenUserNotFound() throws Exception {
when(userService.findUser(999L)).thenThrow(new UserNotFoundException("User not found"));
mockMvc.perform(get("/api/users/999"))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.error").value("User Not Found"));
}
Testing validation exceptions:
@Test
void shouldReturn400ForInvalidUser() throws Exception {
String invalidUser = "{\\"name\\": \\"\\", \\"email\\": \\"invalid-email\\"}";
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(invalidUser))
.andExpected(status().isBadRequest())
.andExpect(jsonPath("$.fieldErrors.name").exists())
.andExpect(jsonPath("$.fieldErrors.email").exists());
}
Testing custom exception handling:
@Test
void shouldHandleInsufficientFunds() throws Exception {
when(paymentService.processPayment(any())).thenThrow(
new InsufficientFundsException(BigDecimal.valueOf(50), BigDecimal.valueOf(100)));
mockMvc.perform(post("/api/payments")
.contentType(MediaType.APPLICATION_JSON)
.content("{\\"amount\\": 100}"))
.andExpect(status().isPaymentRequired())
.andExpect(jsonPath("$.error").value("Payment Failed"));
}
Unit testing exception handlers:
@Test
void globalExceptionHandlerShouldHandleUserNotFound() {
GlobalExceptionHandler handler = new GlobalExceptionHandler();
UserNotFoundException exception = new UserNotFoundException("User not found");
ResponseEntity response = handler.handleUserNotFound(exception);
assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
assertEquals("User not found", response.getBody().getMessage());
}
Best Practices
Definition
Exception handling best practices ensure consistent, secure, and maintainable error management across your application. These include using specific exception types, providing helpful but not sensitive error messages, logging appropriately for debugging while protecting user privacy, using correct HTTP status codes, and testing all error scenarios. Following best practices makes your application more professional, easier to debug, and more secure.
Analogy
Exception handling best practices are like the professional standards that customer service representatives follow when dealing with complaints or problems. Good representatives are trained to listen carefully to understand the specific problem (use specific exception types), provide clear explanations without revealing confidential company information (helpful but secure messages), document the interaction properly for follow-up (appropriate logging), escalate to the right department based on the issue type (correct status codes), and follow up to ensure the problem was resolved (testing). These standards ensure every customer gets consistent, professional service regardless of which representative they speak with, and they protect both the customer and the company during difficult situations.
Examples
Secure error messages:
// Good - helpful but not revealing internal details
@ExceptionHandler(AuthenticationException.class)
public ResponseEntity handleAuth(AuthenticationException ex) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(new ErrorResponse("Authentication Failed", "Invalid username or password"));
}
// Bad - reveals too much internal information
// "Database connection failed: Cannot connect to MySQL server at localhost:3306"
Consistent error response format:
// Use the same response structure for all errors
public class ErrorResponse {
private final String error;
private final String message;
private final LocalDateTime timestamp;
private final String path;
// Always include these core fields for consistency
}
Appropriate logging levels:
// WARN for client errors (400-499)
logger.warn("User validation failed: {}", ex.getMessage());
// ERROR for server errors (500+)
logger.error("Database connection failed", ex);
// DEBUG for detailed troubleshooting info
logger.debug("Request parameters: {}", requestParams);
Exception hierarchy for better handling:
// Base exception for your application
public abstract class BusinessException extends RuntimeException {
protected BusinessException(String message) {
super(message);
}
}
// Specific business exceptions
public class UserNotFoundException extends BusinessException {
public UserNotFoundException(String message) {
super(message);
}
}
Summary
You've now mastered Spring Boot exception handling, from creating global exception handlers to building custom exceptions and error responses. Proper exception handling transforms your application from one that crashes ungracefully or exposes confusing technical details into a professional system that guides users through problems with helpful, consistent error messages. You've learned to use appropriate HTTP status codes, structure error responses consistently, handle validation errors gracefully, and implement security-conscious logging that helps developers debug issues without exposing sensitive information. These skills enable you to build robust applications that maintain user trust even when things go wrong. Next, you'll explore monitoring and caching strategies that help you proactively identify and prevent errors while optimizing application performance.
Programming Challenge
Challenge: Comprehensive Error Handling System
Task: Build a Spring Boot application with comprehensive exception handling for a library management system that demonstrates all the concepts covered in this lesson.
Requirements:
- Create a simple library management application:
Book
,Author
,User
,Loan
entities- REST endpoints for managing books, users, and loans
- Business logic for borrowing and returning books
- Implement custom exceptions:
BookNotFoundException
when book doesn't existBookNotAvailableException
when book is already borrowedUserNotFoundException
when user doesn't existLoanNotFoundException
when loan record doesn't existOverdueLoanException
when returning overdue books- Create global exception handler:
- Handle all custom exceptions with appropriate status codes
- Handle validation exceptions with field-specific error messages
- Handle generic exceptions with secure error responses
- Include structured error responses with timestamps and request IDs
- Add comprehensive validation:
- Validate book creation (title required, ISBN format, publication year)
- Validate user registration (email format, age minimum, name required)
- Validate loan requests (valid dates, user exists, book available)
- Implement security-conscious logging:
- Log errors with appropriate levels (WARN for client errors, ERROR for server errors)
- Include request context without exposing sensitive data
- Use correlation IDs for tracking requests across logs
- Add comprehensive tests:
- Test each exception scenario with appropriate status codes
- Test validation error responses for malformed requests
- Test that error responses don't expose sensitive information
- Test that appropriate logging occurs for different error types
Bonus features:
- Custom error pages for web interface
- Internationalized error messages (English and one other language)
- Rate limiting with custom exception for too many requests
- Circuit breaker pattern for external service calls
- Health check endpoints that report system status
Learning Goals: Practice implementing comprehensive exception handling strategies, creating meaningful custom exceptions, structuring consistent error responses, and building robust error management that enhances user experience while maintaining security and debuggability.