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.

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 tipounknownexiste 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
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.