Learn how to log exceptions safely in Spring Boot without exposing sensitive details to clients. Beginner-friendly, end-to-end Java 21 examples.
Primary Keyword: Logging Exceptions Securely in Spring Boot
Secondary Keywords: Java programming, learn Java, Spring Boot exception handling
Introduction
Imagine visiting your bank’s website and seeing an error like this:
NullPointerException at com.bank.payment.CardService.process(CardService.java:143)
Scary, right? 😨
Not only is it confusing, but it also exposes internal code details—something attackers love.
In Spring Boot, exceptions are inevitable. Databases go down, validations fail, and unexpected bugs happen. The real challenge is logging full details for developers while showing safe, friendly messages to clients.
In this blog, you’ll learn how to log exceptions without exposing sensitive details to clients in Spring Boot, using clear explanations, real-world analogies, and an end-to-end Java 21 example.
Core Concepts
The Golden Rule
Log everything internally, expose nothing sensitive externally.
Think of it like a hospital:
- 🩺 Doctors need full medical reports (logs)
- 🧑🤝🧑 Patients need simple explanations (“You’ll be okay”)
Your API should behave the same way.
What Counts as Sensitive Information?
Never expose:
- Stack traces
- Database details
- Internal class names
- SQL queries
- API keys or tokens
- File paths
Key Spring Boot Tools
- SLF4J Logger – For structured logging
-
@ControllerAdvice– Centralized exception handling -
@ExceptionHandler– Map exceptions to safe responses - Custom Error Responses – Clean, client-friendly messages
Code Examples (End-to-End)
Let’s build a secure exception logging setup step by step.
✅ Example 1: Log Full Exception, Return Safe Response
Step 1: Service Layer (Exception Occurs Here)
package com.example.demo.service;
import org.springframework.stereotype.Service;
@Service
public class PaymentService {
public String processPayment(String cardNumber) {
if (cardNumber == null) {
// Simulating a real failure
throw new IllegalStateException("Card number cannot be null");
}
return "Payment processed successfully";
}
}
Step 2: REST Controller (Clean, No Try-Catch)
package com.example.demo.controller;
import com.example.demo.service.PaymentService;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/payments")
public class PaymentController {
private final PaymentService paymentService;
public PaymentController(PaymentService paymentService) {
this.paymentService = paymentService;
}
@PostMapping
public String makePayment(@RequestParam(required = false) String cardNumber) {
return paymentService.processPayment(cardNumber);
}
}
Step 3: Global Exception Handler (Secure Logging)
package com.example.demo.exception;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.Map;
@ControllerAdvice
public class GlobalExceptionHandler {
private static final Logger log =
LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(IllegalStateException.class)
public ResponseEntity<Map<String, Object>> handleIllegalState(
IllegalStateException ex) {
// 🔒 Log full details for developers
log.error("Payment processing failed", ex);
// 🧑 Client gets safe message only
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(buildSafeErrorResponse(
"Payment could not be processed. Please try again later."));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleGenericException(
Exception ex) {
log.error("Unexpected system error", ex);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(buildSafeErrorResponse(
"Unexpected error occurred. Contact support."));
}
private Map<String, Object> buildSafeErrorResponse(String message) {
return Map.of(
"timestamp", LocalDateTime.now(),
"message", message
);
}
}
🔍 Result
curl --location --request POST 'http://localhost:8080/payment'
{
"message": "Unexpected error occurred. Contact support.",
"timestamp": "2025-12-24T17:42:58.4423334"
}
curl --location --request POST 'http://localhost:8080/payments?cardNumber=null'
{
"message": "Payment could not be processed. Please try again later.",
"timestamp": "2025-12-24T17:43:58.5476755"
}
❌ Client Response
{
"timestamp": "2025-01-01T12:30:00",
"message": "Payment could not be processed. Please try again later."
}
✅ Application Logs
ERROR Payment processing failed
java.lang.IllegalStateException: Card number cannot be null
at com.example.demo.service.PaymentService.processPayment(...)
✔ Sensitive details stay in logs
✔ Clients see clean messages
✅ Example 2: Log Correlation ID for Production Debugging
Why This Matters
In production, logs can be huge. A correlation ID helps you track one request across logs—without exposing internals.
Step 4: Add Correlation ID to Error Response
package com.example.demo.exception;
import org.slf4j.MDC;
import java.util.UUID;
public class CorrelationIdUtil {
public static String getOrCreateCorrelationId() {
String correlationId = MDC.get("correlationId");
if (correlationId == null) {
correlationId = UUID.randomUUID().toString();
MDC.put("correlationId", correlationId);
}
return correlationId;
}
}
Update Global Exception Handler
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleGenericException(Exception ex) {
String correlationId = CorrelationIdUtil.getOrCreateCorrelationId();
log.error("Unexpected error occurred. CorrelationId={}", correlationId, ex);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of(
"timestamp", LocalDateTime.now(),
"message", "Something went wrong. Contact support.",
"correlationId", correlationId
));
}
🔐 Client Response (Safe + Traceable)
{
"message": "Something went wrong. Contact support.",
"correlationId": "7b9e1c9d-3f64-4c9a-9b6f-3f7c8d99a123"
}
Now support teams can debug without leaking sensitive data.
Best Practices
Never return stack traces to clients
Even in dev environments, this builds bad habits.Always log exceptions with context
Uselog.error("message", ex)— not justex.getMessage().Use generic client messages
Friendly, non-technical language works best.Centralize exception handling
Use@ControllerAdviceto avoid duplication.Use correlation IDs in production
Essential for microservices and high-traffic systems.
Conclusion
Exception handling isn’t just about fixing bugs—it’s about security, professionalism, and trust.
By logging full exception details internally and returning safe, controlled responses externally, you protect your application and your users.
Mastering logging exceptions securely in Spring Boot is a must-have skill for anyone serious about Java programming and building real-world APIs.
Call to Action
💬 Have questions about secure logging or exception handling?
👇 Drop them in the comments below!
🔗 Helpful Resources
Top comments (0)