Capítulo 1: Gestión de Excepciones
Introducción
Las excepciones son el mecanismo de Java para señalar errores. Usarlas correctamente es esencial para crear aplicaciones robustas y mantenibles.
🎯 Objetivos del Capítulo
Al final de este capítulo, usted sabrá:
- ✅ Diferenciar entre Checked y Unchecked exceptions
- ✅ Crear excepciones personalizadas
- ✅ Organizar una jerarquía de excepciones
- ✅ Aplicar buenas prácticas
1. Recordatorio: Jerarquía de Excepciones en Java
Throwable
├── Error (no atrapar)
│ ├── OutOfMemoryError
│ └── StackOverflowError
└── Exception
├── Checked Exceptions (deben ser declaradas)
│ ├── IOException
│ └── SQLException
└── RuntimeException (Unchecked)
├── NullPointerException
├── IllegalArgumentException
└── Sus excepciones personalizadas
2. Checked vs Unchecked
Checked Exceptions
// Deben ser declaradas con throws o capturadas con catch
public void readFile(String path) throws IOException {
Files.readString(Path.of(path)); // IOException posible
}
// O
public void readFile(String path) {
try {
Files.readString(Path.of(path));
} catch (IOException e) {
// Manejar el error
}
}
¿Cuándo usar? Errores recuperables (archivo faltante, red).
Unchecked Exceptions (RuntimeException)
// No es necesario declarar
public User findById(Long id) {
if (id == null) {
throw new IllegalArgumentException("ID cannot be null");
}
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}
¿Cuándo usar? Errores de programación, violaciones de contrato.
En Spring Boot: Prefiera Unchecked
// ❌ Pesado de usar
public User findById(Long id) throws UserNotFoundException {
// ...
}
// ✅ Más fluido
public User findById(Long id) {
// Lanza una RuntimeException si no se encuentra
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}
3. Crear Excepciones Personalizadas
Excepción Simple
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(Long id) {
super("User not found with ID: " + id);
}
public UserNotFoundException(String message) {
super(message);
}
}
Excepción con Datos Adicionales
@Getter
public class ResourceNotFoundException extends RuntimeException {
private final String resourceType;
private final Object resourceId;
public ResourceNotFoundException(String resourceType, Object resourceId) {
super(String.format("%s not found with ID: %s", resourceType, resourceId));
this.resourceType = resourceType;
this.resourceId = resourceId;
}
}
// Uso
throw new ResourceNotFoundException("User", 123L);
throw new ResourceNotFoundException("Product", "SKU-001");
Excepción con Código de Error
@Getter
public class BusinessException extends RuntimeException {
private final String errorCode;
private final Map<String, Object> details;
public BusinessException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
this.details = new HashMap<>();
}
public BusinessException(String errorCode, String message, Map<String, Object> details) {
super(message);
this.errorCode = errorCode;
this.details = details;
}
}
// Uso
throw new BusinessException(
"INSUFFICIENT_BALANCE",
"Insufficient balance for withdrawal",
Map.of("requested", 500, "available", 100)
);
4. Jerarquía de Excepciones de Negocio
Estructura Recomendada
// Excepción base para la aplicación
public abstract class ApplicationException extends RuntimeException {
private final String errorCode;
protected ApplicationException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
protected ApplicationException(String errorCode, String message, Throwable cause) {
super(message, cause);
this.errorCode = errorCode;
}
public String getErrorCode() {
return errorCode;
}
}
Excepciones Específicas
// Recurso no encontrado (→ 404)
public class ResourceNotFoundException extends ApplicationException {
public ResourceNotFoundException(String resourceType, Object id) {
super("RESOURCE_NOT_FOUND",
String.format("%s with ID %s not found", resourceType, id));
}
}
// Validación fallida (→ 400)
public class ValidationException extends ApplicationException {
private final List<FieldError> errors;
public ValidationException(List<FieldError> errors) {
super("VALIDATION_FAILED", "Validation failed");
this.errors = errors;
}
}
// Conflicto (→ 409)
public class ConflictException extends ApplicationException {
public ConflictException(String message) {
super("CONFLICT", message);
}
}
// No autorizado (→ 403)
public class ForbiddenException extends ApplicationException {
public ForbiddenException(String message) {
super("FORBIDDEN", message);
}
}
// Error de negocio (→ 422)
public class BusinessRuleException extends ApplicationException {
public BusinessRuleException(String code, String message) {
super(code, message);
}
}
Diagrama
ApplicationException
├── ResourceNotFoundException → 404
├── ValidationException → 400
├── ConflictException → 409
├── ForbiddenException → 403
├── BusinessRuleException → 422
└── TechnicalException → 500
5. Ejemplos de Uso
En un Servicio
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
public User findById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("User", id));
}
public User create(UserRequest request) {
// Verificar unicidad
if (userRepository.existsByEmail(request.getEmail())) {
throw new ConflictException("Email already in use: " + request.getEmail());
}
// Crear el usuario
User user = new User();
user.setEmail(request.getEmail());
user.setName(request.getName());
return userRepository.save(user);
}
public void delete(Long id) {
User user = findById(id); // Lanza ResourceNotFoundException si no se encuentra
if (user.hasActiveOrders()) {
throw new BusinessRuleException(
"CANNOT_DELETE_USER_WITH_ORDERS",
"Cannot delete user with active orders"
);
}
userRepository.delete(user);
}
}
En un Controller (sin gestión centralizada)
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public ResponseEntity<User> getById(@PathVariable Long id) {
try {
User user = userService.findById(id);
return ResponseEntity.ok(user);
} catch (ResourceNotFoundException e) {
return ResponseEntity.notFound().build();
}
}
}
Problema: ¡Repetición del try-catch en cada método!
Solución: @ControllerAdvice (siguiente capítulo)
6. Buenas Prácticas
✅ A Hacer
// 1. Nombres claros
public class InsufficientStockException extends BusinessRuleException { }
public class PaymentDeclinedException extends BusinessRuleException { }
// 2. Incluir el contexto
throw new ResourceNotFoundException("Product", productId);
// 3. Usar códigos de error
throw new BusinessRuleException("ORDER_LIMIT_EXCEEDED", "...");
// 4. Encadenar las causas
try {
externalService.call();
} catch (ExternalServiceException e) {
throw new TechnicalException("External service failed", e);
}
❌ A Evitar
// 1. Excepciones genéricas
throw new Exception("Something went wrong"); // ❌
// 2. Mensajes vacíos
throw new RuntimeException(); // ❌
// 3. Capturar y silenciar
try {
riskyOperation();
} catch (Exception e) {
// Silencioso - ❌ ¡NUNCA!
}
// 4. Excepciones para el flujo normal
try {
user = findById(id);
} catch (UserNotFoundException e) {
user = createUser(id); // ❌ Usar Optional en su lugar
}
Patrón: Optional vs Exception
// Para "puede no existir"
public Optional<User> findByEmail(String email) {
return userRepository.findByEmail(email);
}
// Para "DEBE existir"
public User getById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("User", id));
}
7. Excepciones Nativas de Spring Boot
ResponseStatusException
@GetMapping("/{id}")
public User getById(@PathVariable Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(
HttpStatus.NOT_FOUND,
"User not found: " + id
));
}
Ventaja: Simple, no necesita @ControllerAdvice
Desventaja: Menos flexible, sin formato personalizado
@ResponseStatus en Excepción
@ResponseStatus(HttpStatus.NOT_FOUND)
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(Long id) {
super("User not found: " + id);
}
}
Ventaja: Automático
Desventaja: Formato de respuesta por defecto de Spring
8. Resumen
Tipos de Excepciones
| Tipo | Herencia | Uso |
|---|---|---|
| Checked | Exception | Errores recuperables |
| Unchecked | RuntimeException | Errores de negocio, programación |
Jerarquía Recomendada
ApplicationException (abstract)
├── ResourceNotFoundException (404)
├── ValidationException (400)
├── ConflictException (409)
├── BusinessRuleException (422)
└── TechnicalException (500)
Lista de Verificación
✅ Excepciones heredan de RuntimeException
✅ Mensajes claros e informativos
✅ Códigos de error para la API
✅ Contexto incluido (ID, tipo de recurso)
✅ Causas encadenadas para depuración
🎯 Próximo Paso
Ya sabe crear excepciones. Ahora, aprendamos a gestionarlas globalmente con @ControllerAdvice.
➡️ Capítulo 2: @ControllerAdvice