Scala

Bloques try/catch/finally en Scala

7 min lectura José Miguel

En cualquier lenguaje de programación, el manejo de excepciones es una parte fundamental para escribir código robusto. Scala ofrece un mecanismo de manejo de excepciones que resultará familiar si vienes de Java, pero con diferencias importantes en la sintaxis que lo hacen más expresivo y conciso.

Los bloques try/catch/finally de Scala son similares a los de Java pero la sintaxis es un poco diferente, principalmente en el bloque catch debido a que es similar a una expresión match.

Para entender bien cómo funciona el bloque catch en Scala, primero necesitamos comprender las expresiones match, ya que el catch utiliza la misma sintaxis de pattern matching.

Expresiones match

Las expresiones match son una característica de Scala. Al igual que las expresiones if, las expresiones match retornan un valor por lo que las puedes usar como el cuerpo de un método.

A diferencia del switch de Java, las expresiones match en Scala son mucho más poderosas: pueden hacer coincidir tipos, patrones complejos y, lo más importante, siempre retornan un valor. Esto las convierte en una herramienta esencial en la programación funcional.

A continuación veamos un ejemplo de una expresión match.

Nota importante: Los ejemplos mostrados en este artículo fueron probados con Scala 3.

def verdadero(a: Matchable): Boolean = a match
  case false => false
  case 0     => false
  case ""    => false
  case _     => true

En este ejemplo, si la función verdadero recibe un valor false, el valor 0 o un string vacío esta retornará false, en cualquier otro caso retornará true.

Observa que el parámetro a tiene el tipo Matchable. Este es un tipo introducido en Scala 3 que indica que el valor puede ser utilizado en una expresión de pattern matching. En versiones anteriores de Scala se usaba Any para este propósito.

También observa que en Scala 3 no es necesario usar llaves {} después de match. La indentación es suficiente para delimitar los bloques de código, lo que resulta en una sintaxis más limpia y legible.

El caso case _ actúa como un comodín (wildcard) que captura cualquier valor que no haya sido coincidido por los casos anteriores. Es similar al default en un switch de Java.

Una vez que hemos visto como funcionan las expresiones match estamos en condiciones de ver entonces cómo funcionan los bloques try/catch/finally en Scala.

Bloques try/catch/finally

La forma general de los bloques try/catch/finally es la siguiente:

try
  // código que puede lanzar una excepción
catch
  case e: ExcepcionTipo1 => // manejar excepción tipo 1
  case e: ExcepcionTipo2 => // manejar excepción tipo 2
finally
  // código que siempre se ejecuta

Como puedes ver, el bloque catch utiliza la misma sintaxis de case que las expresiones match. Cada case captura un tipo de excepción diferente, y puedes acceder al objeto de la excepción a través de la variable (en este caso e) para obtener información como el mensaje de error.

try como expresión

Similar a if y match, try es una expresión que retorna un valor, así que puedes escribir código, como por ejemplo, transformar un String a un Int como se muestra a continuación:

def convertirEntero(s: String): Int =
  try
    s.toInt
  catch
    case e: NumberFormatException => 0

En esta función, si el String recibido puede convertirse a Int, el método toInt retorna el valor numérico. Si no es posible (por ejemplo, si el string contiene letras), toInt lanza una NumberFormatException y el bloque catch captura esa excepción retornando 0 como valor por defecto.

Los siguientes ejemplos muestran cómo trabaja la función convertirEntero:

println(convertirEntero("1"))   // 1
println(convertirEntero("a"))   // 0
println(convertirEntero("100")) // 100

Dado que try es una expresión, también puedes asignar su resultado directamente a una variable:

val resultado: Int = try
  "42".toInt
catch
  case e: NumberFormatException => 0

println(resultado) // 42

Capturar múltiples excepciones

En aplicaciones reales es común que un bloque de código pueda lanzar diferentes tipos de excepciones. Puedes capturar cada tipo de forma individual usando múltiples cláusulas case:

def leerArchivo(ruta: String): String =
  try
    val fuente = scala.io.Source.fromFile(ruta)
    try
      fuente.mkString
    finally
      fuente.close()
  catch
    case e: java.io.FileNotFoundException =>
      s"Error: El archivo '$ruta' no fue encontrado."
    case e: java.io.IOException =>
      s"Error de lectura: ${e.getMessage}"
    case e: Exception =>
      s"Error inesperado: ${e.getMessage}"

En este ejemplo capturamos tres tipos de excepciones de forma específica. Es importante notar que el orden de los case importa: Scala evalúa los casos de arriba hacia abajo y ejecuta el primero que coincida. Por esta razón, los tipos más específicos deben ir primero y los más generales al final.

El bloque finally

El bloque finally se ejecuta siempre, sin importar si el código del try lanzó una excepción o no. Esto lo hace ideal para liberar recursos como conexiones a bases de datos, archivos abiertos o cualquier recurso que necesite ser cerrado de forma segura:

import java.io.{FileWriter, IOException}

def escribirArchivo(ruta: String, contenido: String): Unit =
  var escritor: FileWriter = null
  try
    escritor = new FileWriter(ruta)
    escritor.write(contenido)
  catch
    case e: IOException =>
      println(s"Error al escribir: ${e.getMessage}")
  finally
    if escritor != null then
      escritor.close()

En este ejemplo, el FileWriter se cierra en el bloque finally sin importar si la escritura fue exitosa o si ocurrió un error. Esto garantiza que no queden recursos abiertos que puedan causar fugas de memoria o bloqueos de archivos.

Buenas prácticas

A continuación se listan algunas buenas prácticas al trabajar con try/catch/finally en Scala:

  • Captura excepciones específicas: Evita capturar Exception o Throwable de forma genérica. Siempre captura el tipo de excepción más específico posible para no ocultar errores inesperados.
  • No ignores excepciones silenciosamente: Un catch vacío que no hace nada con la excepción es una de las peores prácticas. Como mínimo, registra el error con un log o un println.
  • Usa finally para limpiar recursos: Si abres un archivo, una conexión de red o cualquier recurso que necesite ser liberado, hazlo siempre en el bloque finally.
  • Considera alternativas funcionales: Scala ofrece Try, Option y Either como alternativas funcionales al try/catch tradicional. Por ejemplo, scala.util.Try encapsula el resultado de una operación que puede fallar:
import scala.util.{Try, Success, Failure}

val resultado: Try[Int] = Try("42".toInt)

resultado match
  case Success(valor) => println(s"Valor: $valor")
  case Failure(error) => println(s"Error: ${error.getMessage}")

Try es especialmente útil cuando trabajas con colecciones o composición funcional, ya que se integra perfectamente con métodos como map, flatMap y getOrElse.

Errores comunes

Estos son algunos errores frecuentes al utilizar try/catch/finally en Scala:

  • Olvidar el caso comodín: Si no incluyes un case _ o un case e: Exception como último caso, cualquier excepción no contemplada se propagará sin ser capturada. Esto puede ser intencional, pero asegúrate de que sea una decisión consciente.
  • Retornar valores en finally: Evita retornar valores desde el bloque finally. Si tanto el try como el finally retornan un valor, el valor del finally sobreescribe al del try, lo que puede producir resultados inesperados:
def ejemplo(): Int =
  try
    1
  finally
    2 // Este valor NO se retorna (pero en algunos casos sí podría, causando confusión)
  • Capturar excepciones demasiado amplias: Capturar Throwable incluye errores del sistema como OutOfMemoryError o StackOverflowError, que generalmente no deberían ser capturados porque indican problemas graves en la JVM.

Conclusión

Con estos ejemplos hemos visto cómo funcionan los bloques try/catch/finally en Scala. La clave está en recordar que el catch utiliza pattern matching, lo que lo hace más expresivo que en Java, y que try es una expresión que retorna un valor, lo que te permite usarlo en asignaciones y como cuerpo de métodos.

José Miguel Moya Curbelo
José Miguel Moya Curbelo
Senior Data Engineer & Big Data Instructor

MSc Applied Mathematics · AWS Cloud Practitioner · SCRUM Master. Especializado en arquitecturas de datos de alto rendimiento con Apache Spark, Snowflake, Python y Scala.

Conectar en LinkedIn

Artículos Relacionados

Deja un comentario

Tu dirección de correo electrónico no será publicada.