Manejo de errores tipado en TypeScript

Avanzado
TypeScript
TypeScript
Actualizado: 18/04/2026

Try-catch con unknown

En TypeScript con strict: true, el parámetro de un bloque catch tiene tipo unknown. Esto obliga a verificar el tipo antes de acceder a cualquier propiedad, eliminando accesos inseguros que podrían causar errores secundarios.

Patrón Result para manejo de errores tipado

function parsearJSON(texto: string): unknown {
  try {
    return JSON.parse(texto)
  } catch (error) {
    // error es unknown: no se puede acceder a .message directamente
    // console.error(error.message) // Error de compilación

    if (error instanceof Error) {
      console.error(`Error de parseo: ${error.message}`)
    } else {
      console.error(`Error desconocido: ${String(error)}`)
    }
    return null
  }
}

parsearJSON('{"nombre": "Ana"}')   // Correcto
parsearJSON('texto inválido')       // Error de parseo: ...

Para reutilizar la extracción del mensaje de error, se puede crear una función auxiliar:

function obtenerMensajeError(error: unknown): string {
  if (error instanceof Error) {
    return error.message
  }
  if (typeof error === "string") {
    return error
  }
  if (
    typeof error === "object" &&
    error !== null &&
    "message" in error &&
    typeof (error as { message: unknown }).message === "string"
  ) {
    return (error as { message: string }).message
  }
  return "Error desconocido"
}

try {
  throw new Error("Fallo de conexión")
} catch (error) {
  console.error(obtenerMensajeError(error))
}

try {
  throw "algo salio mal"
} catch (error) {
  console.error(obtenerMensajeError(error))
}

Nunca uses catch (error: any) para evitar el tipado seguro. El tipo unknown existe para forzar la verificación explícita, y omitirlo anula una de las protecciones más importantes de TypeScript estricto.

Clases de error personalizadas

Las clases de error personalizadas permiten categorizar errores por dominio y añadir propiedades tipadas relevantes para cada tipo de fallo:

class ErrorValidacion extends Error {
  readonly tipo = "validación" as const

  constructor(
    public readonly campo: string,
    public readonly valorRecibido: unknown,
    mensaje: string
  ) {
    super(mensaje)
    this.name = "ErrorValidacion"
    Object.setPrototypeOf(this, new.target.prototype)
  }
}

class ErrorAPI extends Error {
  readonly tipo = "api" as const

  constructor(
    public readonly statusCode: number,
    public readonly endpoint: string,
    mensaje: string
  ) {
    super(mensaje)
    this.name = "ErrorAPI"
    Object.setPrototypeOf(this, new.target.prototype)
  }
}

class ErrorPermisos extends Error {
  readonly tipo = "permisos" as const

  constructor(
    public readonly accion: string,
    public readonly recurso: string
  ) {
    super(`Sin permisos para ${accion} en ${recurso}`)
    this.name = "ErrorPermisos"
    Object.setPrototypeOf(this, new.target.prototype)
  }
}

class ErrorNoEncontrado extends Error {
  readonly tipo = "no_encontrado" as const

  constructor(
    public readonly entidad: string,
    public readonly identificador: string
  ) {
    super(`${entidad} con id ${identificador} no encontrado`)
    this.name = "ErrorNoEncontrado"
    Object.setPrototypeOf(this, new.target.prototype)
  }
}

La llamada a Object.setPrototypeOf(this, new.target.prototype) es necesaria para que instanceof funcione correctamente cuando se compila a versiones de JavaScript anteriores a ES2015.

Narrowing con instanceof

type ErrorDominio = ErrorValidacion | ErrorAPI | ErrorPermisos | ErrorNoEncontrado

function manejarError(error: ErrorDominio): string {
  if (error instanceof ErrorValidacion) {
    return `Campo "${error.campo}" inválido: ${error.message}`
  }
  if (error instanceof ErrorAPI) {
    return `Error HTTP ${error.statusCode} en ${error.endpoint}`
  }
  if (error instanceof ErrorPermisos) {
    return `Acceso denegado: ${error.accion} en ${error.recurso}`
  }
  if (error instanceof ErrorNoEncontrado) {
    return `${error.entidad} no encontrado: ${error.identificador}`
  }

  const _exhaustivo: never = error
  throw new Error("Tipo de error no manejado")
}

console.log(manejarError(new ErrorValidacion("email", "abc", "Formato inválido")))
console.log(manejarError(new ErrorAPI(404, "/api/usuarios", "No encontrado")))
console.log(manejarError(new ErrorPermisos("eliminar", "articulo:42")))

El patrón Result

El patrón Result modela operaciones que pueden fallar como valores en lugar de excepciones. Inspirado en lenguajes como Rust, utiliza una discriminated union para representar exito o fallo:

type Result<T, E = Error> =
  | { ok: true, valor: T }
  | { ok: false, error: E }

function dividir(a: number, b: number): Result<number> {
  if (b === 0) {
    return { ok: false, error: new Error("Division por cero") }
  }
  return { ok: true, valor: a / b }
}

const resultado = dividir(10, 3)

if (resultado.ok) {
  console.log(`Resultado: ${resultado.valor}`) // TypeScript sabe que es number
} else {
  console.error(`Error: ${resultado.error.message}`) // TypeScript sabe que es Error
}

El patrón Result hace explícito en la firma de la función que una operación puede fallar. A diferencia de las excepciones, el compilador obliga a manejar ambos casos, eliminando errores no capturados.

Result con errores de dominio

Combinar Result con las clases de error personalizadas permite un manejo granular:

type Result<T, E = Error> =
  | { ok: true, valor: T }
  | { ok: false, error: E }

interface Usuario {
  id: string
  nombre: string
  email: string
}

type ErrorUsuario = ErrorValidacion | ErrorAPI | ErrorNoEncontrado

async function obtenerUsuario(id: string): Promise<Result<Usuario, ErrorUsuario>> {
  if (!id.trim()) {
    return {
      ok: false,
      error: new ErrorValidacion("id", id, "El ID no puede estar vacío")
    }
  }

  try {
    const response = await fetch(`/api/usuarios/${id}`)

    if (response.status === 404) {
      return {
        ok: false,
        error: new ErrorNoEncontrado("Usuario", id)
      }
    }

    if (!response.ok) {
      return {
        ok: false,
        error: new ErrorAPI(response.status, `/api/usuarios/${id}`, "Error del servidor")
      }
    }

    const usuario = await response.json() as Usuario
    return { ok: true, valor: usuario }
  } catch (error) {
    return {
      ok: false,
      error: new ErrorAPI(0, `/api/usuarios/${id}`, obtenerMensajeError(error))
    }
  }
}

Encadenar operaciones con Result

type Result<T, E = Error> =
  | { ok: true, valor: T }
  | { ok: false, error: E }

function mapResult<T, U, E>(
  resultado: Result<T, E>,
  transformar: (valor: T) => U
): Result<U, E> {
  if (resultado.ok) {
    return { ok: true, valor: transformar(resultado.valor) }
  }
  return resultado
}

function flatMapResult<T, U, E>(
  resultado: Result<T, E>,
  transformar: (valor: T) => Result<U, E>
): Result<U, E> {
  if (resultado.ok) {
    return transformar(resultado.valor)
  }
  return resultado
}

function parsearEntero(texto: string): Result<number> {
  const número = parseInt(texto, 10)
  if (isNaN(número)) {
    return { ok: false, error: new Error(`"${texto}" no es un entero válido`) }
  }
  return { ok: true, valor: número }
}

function validarRango(min: number, max: number) {
  return function (valor: number): Result<number> {
    if (valor < min || valor > max) {
      return { ok: false, error: new Error(`${valor} fuera del rango [${min}, ${max}]`) }
    }
    return { ok: true, valor }
  }
}

const resultado = flatMapResult(
  parsearEntero("42"),
  validarRango(1, 100)
)

const duplicado = mapResult(resultado, (n) => n * 2)

if (duplicado.ok) {
  console.log(`Resultado: ${duplicado.valor}`) // 84
} else {
  console.error(duplicado.error.message)
}

Never para código inalcanzable

El tipo never representa valores que nunca ocurren. Es fundamental para garantizar que un switch o cadena de if/else maneja todos los casos posibles de una union:

type EstadoPedido = "pendiente" | "enviado" | "entregado" | "cancelado"

function procesarEstado(estado: EstadoPedido): string {
  switch (estado) {
    case "pendiente":
      return "El pedido está pendiente de envio"
    case "enviado":
      return "El pedido está en camino"
    case "entregado":
      return "El pedido ha sido entregado"
    case "cancelado":
      return "El pedido fue cancelado"
    default:
      // Si se añade un nuevo estado a EstadoPedido sin actualizar este switch,
      // TypeScript producira un error de compilación aquí
      const _exhaustivo: never = estado
      throw new Error(`Estado no manejado: ${_exhaustivo}`)
  }
}

Función auxiliar para exhaustividad

function verificarExhaustivo(valor: never, mensaje?: string): never {
  throw new Error(mensaje ?? `Caso no manejado: ${valor}`)
}

type Operación = "crear" | "leer" | "actualizar" | "eliminar"

function ejecutar(operación: Operación): string {
  switch (operación) {
    case "crear":
      return "Creando recurso"
    case "leer":
      return "Leyendo recurso"
    case "actualizar":
      return "Actualizando recurso"
    case "eliminar":
      return "Eliminando recurso"
    default:
      return verificarExhaustivo(operación)
  }
}

Si en el futuro se añade una nueva variante a Operación (por ejemplo "archivar"), el compilador senalara un error en verificarExhaustivo porque "archivar" no es asignable a never.

Manejo exhaustivo de errores con discriminated unions

Combinando errores tipados con never, se puede construir un sistema donde el compilador obliga a manejar cada tipo de error:

type ErrorAplicacion =
  | { tipo: "red", mensaje: string, reintentable: boolean }
  | { tipo: "validación", campo: string, mensaje: string }
  | { tipo: "autorizacion", recurso: string }
  | { tipo: "no_encontrado", entidad: string, id: string }

function manejarErrorAplicacion(error: ErrorAplicacion): string {
  switch (error.tipo) {
    case "red":
      if (error.reintentable) {
        return `Error de red (reintentable): ${error.mensaje}`
      }
      return `Error de red fatal: ${error.mensaje}`

    case "validación":
      return `Campo "${error.campo}" inválido: ${error.mensaje}`

    case "autorizacion":
      return `Sin acceso a: ${error.recurso}`

    case "no_encontrado":
      return `${error.entidad} con id ${error.id} no existe`

    default:
      const _exhaustivo: never = error
      throw new Error(`Error no manejado: ${JSON.stringify(_exhaustivo)}`)
  }
}

Sistema completo con Result y errores discriminados

type Result<T, E> =
  | { ok: true, valor: T }
  | { ok: false, error: E }

type ErrorRegistro =
  | { tipo: "email_duplicado", email: string }
  | { tipo: "contrasena_debil", requisitos: string[] }
  | { tipo: "nombre_invalido", razon: string }

interface UsuarioRegistrado {
  id: string
  nombre: string
  email: string
}

function validarNombre(nombre: string): Result<string, ErrorRegistro> {
  if (nombre.length < 2) {
    return {
      ok: false,
      error: { tipo: "nombre_invalido", razon: "Minimo 2 caracteres" }
    }
  }
  return { ok: true, valor: nombre.trim() }
}

function validarContrasena(contrasena: string): Result<string, ErrorRegistro> {
  const requisitos: string[] = []

  if (contrasena.length < 8) requisitos.push("Minimo 8 caracteres")
  if (!/[A-Z]/.test(contrasena)) requisitos.push("Al menos una mayuscula")
  if (!/[0-9]/.test(contrasena)) requisitos.push("Al menos un número")

  if (requisitos.length > 0) {
    return {
      ok: false,
      error: { tipo: "contrasena_debil", requisitos }
    }
  }
  return { ok: true, valor: contrasena }
}

function registrarUsuario(
  nombre: string,
  email: string,
  contrasena: string
): Result<UsuarioRegistrado, ErrorRegistro> {
  const nombreValidado = validarNombre(nombre)
  if (!nombreValidado.ok) return nombreValidado

  const contrasenaValidada = validarContrasena(contrasena)
  if (!contrasenaValidada.ok) return contrasenaValidada

  // Simulacion de email duplicado
  const emailsExistentes = ["ana@ejemplo.com", "luis@ejemplo.com"]
  if (emailsExistentes.includes(email)) {
    return {
      ok: false,
      error: { tipo: "email_duplicado", email }
    }
  }

  return {
    ok: true,
    valor: {
      id: crypto.randomUUID(),
      nombre: nombreValidado.valor,
      email
    }
  }
}

// Uso con manejo exhaustivo
const resultado = registrarUsuario("Ana", "ana@ejemplo.com", "Pass1234")

if (resultado.ok) {
  console.log(`Usuario registrado: ${resultado.valor.id}`)
} else {
  switch (resultado.error.tipo) {
    case "email_duplicado":
      console.error(`El email ${resultado.error.email} ya está registrado`)
      break
    case "contrasena_debil":
      console.error(`Contrasena debil. Requisitos: ${resultado.error.requisitos.join(", ")}`)
      break
    case "nombre_invalido":
      console.error(`Nombre inválido: ${resultado.error.razon}`)
      break
    default:
      const _exhaustivo: never = resultado.error
      throw new Error(`Error no manejado: ${JSON.stringify(_exhaustivo)}`)
  }
}

Funciones que nunca retornan

Las funciones que siempre lanzan excepciones o entran en bucles infinitos tienen tipo de retorno never. Esto es útil para funciones de utilidad que senalizan fallos irrecuperables:

function lanzarError(mensaje: string): never {
  throw new Error(mensaje)
}

function fallarSiNulo<T>(valor: T | null | undefined, mensaje: string): T {
  if (valor === null || valor === undefined) {
    lanzarError(mensaje)
  }
  return valor
}

interface Configuración {
  apiUrl?: string
  apiKey?: string
  timeout?: number
}

function inicializar(config: Configuración) {
  const apiUrl = fallarSiNulo(config.apiUrl, "apiUrl es obligatorio")
  const apiKey = fallarSiNulo(config.apiKey, "apiKey es obligatorio")
  const timeout = config.timeout ?? 5000

  console.log(`API: ${apiUrl}`)
  console.log(`Key: ${apiKey}`)
  console.log(`Timeout: ${timeout}ms`)
}

inicializar({ apiUrl: "https://api.ejemplo.com", apiKey: "abc123" })

try {
  inicializar({ apiUrl: "https://api.ejemplo.com" })
} catch (error) {
  console.error(obtenerMensajeError(error)) // apiKey es obligatorio
}

TypeScript entiende que después de una llamada a una función con tipo never, el código posterior es inalcanzable. Esto permite al compilador refinar tipos automáticamente:

function procesarValor(valor: string | number) {
  if (typeof valor === "string") {
    console.log(valor.toUpperCase())
    return
  }

  if (typeof valor === "number") {
    console.log(valor.toFixed(2))
    return
  }

  // TypeScript sabe que valor es never aquí
  lanzarError(`Tipo inesperado: ${typeof valor}`)
}

El manejo de errores tipado transforma los errores de un mecanismo impredecible basado en excepciones en un flujo de datos explícito que el compilador puede verificar. La combinación de Result, clases de error discriminadas y verificación exhaustiva con never proporciona garantias en tiempo de compilación de que todos los escenarios de error están cubiertos.

Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, TypeScript es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de TypeScript

Explora más contenido relacionado con TypeScript y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

Implementar el patrón Result<T, E> como alternativa tipada a excepciones. Aplicar narrowing con unknown en bloques catch. Crear clases de error personalizadas con tipos discriminados. Usar never para verificar exhaustividad en el manejo de errores.