1. Que es un stack trace y por que es mas importante de lo que parece
Cuando un programa falla, dan ganas de preguntar: «Bueno, y quien hizo esto?». Stack trace (tambien stack trace / stack backtrace / call stack en el momento del fallo) — es justamente el «protocolo de interrogatorio»: una lista de funciones que se llamaron unas a otras hasta llegar a la linea donde todo se rompio.
Es importante entender una cosa sencilla: la linea en la que el programa falla suele ser el lugar donde se manifiesta el sintoma, no la causa raiz. Ejemplo de la vida real: pisaste un LEGO (sintoma), pero la causa raiz es quien dejo el LEGO en el suelo (normalmente tu, ayer). El stack trace sirve precisamente para ver «quien nos trajo hasta aqui».
En Swift, un stack trace aparece en dos situaciones. La primera es cuando el programa realmente «falla» (trap): por ejemplo, Int("abc")!, acceso fuera de los limites del array, fatalError, precondition(false). La segunda es cuando te detienes en el depurador en un breakpoint y miras el call stack en la IDE: ahi esta la misma lista, solo que sin la dramatica caida.
2. Modelo de la pila de llamadas: «un monton de platos» y stack frame
Para que el stack trace no parezca «un hechizo magico en elfico», hace falta un modelo claro. Imagina una pila de platos. Cada funcion es un plato nuevo que colocamos arriba cuando entramos en la funcion. Cuando la funcion termina, retiramos el plato y volvemos al anterior. En programacion, a ese «plato» se le llama stack frame (frame de la pila): ahi estan los parametros de la funcion, las variables locales y la direccion de retorno (donde continuar la ejecucion despues de return).
La mecanica es esta: main llama a runCLI(), esta llama a handle(command:), esa llama a parse(...) y asi sucesivamente. Cuanto mas profundizamos en la logica, mas alta es la pila.
Pequeno esquema (muy simplificado):
flowchart TD
A["main (punto de entrada)"] --> B["runCLI()"]
B --> C["readCommandLine()"]
B --> D["parseCommand(...)"]
D --> E["handle(command:)"]
E --> F["addBook(...)"]
F --> G["aqui ocurrio el crash/trap"]
El sentido del stack trace es mostrarte esta cadena «A → B → C → ... → G», para que puedas responder dos preguntas: donde caimos y quien nos llevo hasta ese lugar.
3. Como leer un stack trace: la parte superior e inferior de la cadena
En el momento del fallo, el stack trace suele mostrarse de arriba abajo, y la primera linea (o las primeras varias) es el lugar donde la ejecucion se detuvo. Ese es el frame superior de la pila: «ahora estamos dentro de esta funcion, en esta linea».
Y debajo vienen los «padres»: quien llamo a la funcion actual, quien llamo a quien llamo..., y asi hasta llegar al punto de entrada del programa y a los frames del sistema.
La forma practica de leerlo es esta. Primero encuentra el primer frame que pertenezca a tu codigo (tu modulo/tu archivo/tus funciones). Todo lo que pertenece al runtime de Swift y a las bibliotecas del sistema no es, por si mismo, «malo»; simplemente casi nunca es el lugar donde te equivocaste. Luego avanzas por tus frames de abajo arriba y te preguntas: «Con que argumentos llegue aqui? Donde se formaron?».
Hay otro matiz importante. A veces la parte superior de la pila dira algo como “Array index out of range” — es cierto, pero inutil si no sabes que indice y por que se volvio asi. Por eso, una buena depuracion por la pila casi siempre termina con que bajas al frame siguiente y miras de donde vino el indice.
4. Mini ejemplo: hacemos un fallo y leemos la «ruta» por la pila
Ahora haremos un pequeno fragmento, parecido a lo que ya has escrito muchas veces en el curso: «CLI lee una linea → analiza la orden → ejecuta». Dejaremos a proposito una «mina» — la extraccion forzada !, para obtener un fallo honesto y ver el stack trace.
Imaginemos que tenemos un comando para anadir un libro con un ano, y decidimos «ahorrar tiempo» (el error favorito de la humanidad) y escribimos !.
import Foundation
func parseYear(_ text: String) -> Int {
Int(text)! // 💥 if text is not a number, we will crash
}
func addBook(title: String, yearText: String) {
let year = parseYear(yearText)
print("Added: \(title) (\(year))")
}
func handleAddCommand(_ line: String) {
let parts = line.split(separator: " ")
let title = String(parts[1])
let yearText = String(parts[2])
addBook(title: title, yearText: yearText)
}
handleAddCommand("add Dune oops") // year is not a number — we provoke a crash
Este codigo compila. Y precisamente por eso es peligroso: el compilador no tiene por que salvarnos de decisiones logicas del tipo «vamos a poner aqui un !».
Como leer el stack trace cuando esto falle? En el veras la cadena de funciones, y entre ellas apareceran las tuyas: parseYear, addBook, handleAddCommand, y luego algo como main.
Tu objetivo es encontrar el frame superior con tu codigo (probablemente parseYear) y entender: «Bien, caimos en Int(text)!». Pero no debes detenerte ahi. Bajas un frame (addBook) y ves: alli llego yearText = "oops". Luego otro mas abajo (handleAddCommand) y entiendes: tomaste el tercer token de la cadena como ano, pero el usuario introdujo algo que no es un numero.
Aqui, el stack trace no dice «quien tiene la culpa» (culpa del !), sino «como llegaron los datos hasta el error».
5. Por que la causa raiz a menudo no esta en el frame superior
Una trampa muy comun para principiantes es arreglar el frame superior para que «no falle». Por ejemplo, reemplazar Int(text)! por Int(text) ?? 0. El programa dejara de fallar, y eso parecera una victoria. Pero muchas veces sera una derrota, solo que disfrazada con un cero.
Imagina: anades un libro, el ano se convierte en 0, el libro va al almacen, luego la ordenacion por ano funciona raro, los filtros muestran «libros del ano 0», y mas adelante todo se rompe ya sin una causa obvia. Y todo empezo porque no quisimos manejar el error con honestidad.
La forma correcta de pensar es esta: el frame superior es el lugar donde «exploto». El siguiente frame es el lugar donde «trajeron la bomba». Y uno mas abajo es el lugar donde «montaron la bomba». Si solo arreglas el lugar de la explosion sin entender quien la trajo y quien la monto, solo estas moviendo el problema.
Para un CLI de aprendizaje, las fuentes de problemas mas comunes son el analisis, las conversiones de tipos, los indices y las suposiciones sobre el numero de tokens. Por eso, al leer el stack trace, busca en la cadena lugares como split, Int(...), array[i], dict[key]! y cualquier operacion «peligrosa».
6. Fallos tipicos en Swift y como se ven en la pila
El stack trace se vuelve mas facil de leer cuando reconoces patrones tipicos. Cada fallo tiene una «forma», y por la forma a menudo puedes adivinar de antemano donde mirar.
Si ves un fallo alrededor de Optional (!), lo mas habitual es que el frame superior apunte a la extraccion forzada. Pero el trabajo real es averiguar por que aparecio nil: una cadena vacia de readLine(), una clave ausente en Dictionary, un formato de numero incorrecto.
Si ves algo como «Index out of range», el frame superior puede estar dentro de la biblioteca estandar, porque ahi es donde se comprueba el limite. Tu tarea es encontrar el frame donde hiciste array[index] y mirar index y array.count. Casi siempre la causa es el rango 0...array.count en lugar de 0..<array.count o la logica de actualizacion del indice en el ciclo.
Si ves fatalError(...) o precondition(...), normalmente tienes suerte: en el mensaje ya se escribe exactamente que se esperaba. Pero incluso aqui la pila importa: te mostrara quien llevo al programa a un «estado imposible».
7. Tu codigo, frames del sistema y demangle
Como «desmitificar» nombres raros en la pila
A veces el stack trace se ve amable: parseYear(_:) y numeros de linea. Y a veces parece que alguien tiro un teclado sobre un diccionario elfico: $s4test28myFunctionDoingTheAllocationyyF. Eso no es magia, es name mangling — una forma de codificar nombres de funciones y tipos en simbolos del binario.
La buena noticia: no hace falta entenderlo con la vista. Hace falta saber descifrarlo con una herramienta. En Swift existe la utilidad swift demangle, que convierte un nombre «codificado» en uno legible. Esto es especialmente util cuando el stack trace no viene de la IDE, sino de una herramienta externa (por ejemplo, un profiler o un analizador de errores), donde los simbolos pueden no ser tan «bonitos».
Recordatorio breve (no lo ejecutamos, solo retenemos la idea): copias el simbolo y lo demanglas.
// This is not Swift code, but the idea of a terminal command:
// swift demangle $s4test28myFunctionDoingTheAllocationyyF
Como separar rapidamente tu codigo de los frames del sistema
En un proyecto real, la mitad de la pila son llamadas del sistema. Y eso es normal: no ejecutas el programa en una calculadora, sino sobre un ecosistema entero.
Para no hundirte, conviene seguir una regla sencilla: «me interesan los frames donde estan mis archivos/mi modulo/mis funciones». En la IDE, normalmente esto se resalta: ves una lista de frames y entre ellos una parte pertenece a tu target.
Aqui tienes una tabla util de «que suele significar un frame» — no como ley estricta, sino como referencia:
| Que ves en el frame | Que significa normalmente | Que hacer |
|---|---|---|
| El nombre de tu funcion y tu archivo | Estas en tu propio codigo | Es el principal candidato para buscar la causa |
| Swift/libswiftCore/runtime | La biblioteca estandar esta atrapando tu error | Sube hacia tu frame, donde llamaste a la operacion peligrosa |
| main | Punto de entrada, «inicio de la ruta» | Es el fondo de la pila, util para contexto, pero rara vez la causa |
| Simbolos extraños $s... | Mangling | Si hace falta, demangla con swift demangle |
El objetivo no es aprenderse los nombres de las funciones del sistema, sino poder decir rapido: «ajá, aqui empieza mi codigo — pues desde aqui trabajamos».
8. Stack trace y recursion: cuando la pila se repite
La recursion es una especie poco comun en un CLI practico, pero como tema de estudio ya la hemos visto, y el stack trace para la recursion es especialmente ilustrativo. Muestra literalmente «como la funcion se llama a si misma una y otra vez». Si el caso base es incorrecto, veras muchisimos frames identicos.
Mini ejemplo que compila y ayuda a ver la repeticion de frames en un breakpoint (o cuando hay un problema con el caso base):
import Foundation
func factorial(_ n: Int) -> Int {
if n <= 1 { return 1 }
return n * factorial(n - 1) // stack: factorial → factorial → factorial ...
}
print(factorial(4)) // 24
Si por accidente hubieras escrito return n * factorial(n + 1), la pila creceria hasta desbordarse (stack overflow), y por los frames repetidos de factorial se veria enseguida: «no nos estamos acercando al caso base».
9. Practica breve: tokens y error de limites de array
Uno de los fallos mas frecuentes en el analisis de CLI es un limite incorrecto de un array de tokens. Y casi siempre se ve en la pila si sabes donde mirar.
Por ejemplo, este codigo compila y puede fallar si el comando es corto:
import Foundation
func handleCommand(_ line: String) {
let parts = line.split(separator: " ")
let command = parts[0]
let arg = parts[1] // 💥 if there is no argument — index out of range
print("cmd=\(command), arg=\(arg)")
}
handleCommand("help")
En el stack trace, probablemente veras algo sobre acceso fuera de los limites. Pero luego es importante encontrar el frame con parts[1]. Ese sera el «punto del sintoma». Y el «punto de la causa raiz» sera tu suposicion de que «el comando siempre tiene un argumento».
Esa suposicion hay que corregirla no con magia, sino con una validacion normal de la entrada (mediante guard, throws, Result — lo que encaje mejor con la arquitectura del curso).
10. Errores tipicos al leer un stack trace
Error n. 1: arreglar la primera linea sin mirar la cadena de llamadas.
Apetece mucho ver la linea del fallo y reescribirla de inmediato para que «no falle». Pero la pila casi siempre te cuenta quien paso los malos datos. Si no miras el frame de abajo, corres el riesgo de esconder el problema bajo ?? 0 o bajo un «return silencioso», y luego perseguir errores extraños en otro sitio.
Error n. 2: asustarse de los frames del sistema y pensar que «se rompio la biblioteca estandar».
La biblioteca estandar rara vez se rompe por tu culpa personal (y sin ti ya ha sobrevivido a mucho). Mas a menudo solo informa con honestidad: «has hecho invalido un indice» o «has desenrollado nil». En la pila hay que encontrar el primer frame donde aparece tu codigo, y empezar la investigacion desde alli.
Error n. 3: leer la pila como una «lista seguida» y no entender que los frames son contextos.
Cada stack frame es un contexto distinto: sus propios parametros, sus variables locales, su propio lugar en el programa. Si cambias entre frames (en la IDE), no olvides que los «valores de las variables» cambian junto con el frame. Una confusion muy comun es mirar la variable en el frame equivocado y sacar una conclusion erronea.
Error n. 4: ignorar los simbolos mangled y pensar que la pila es «ilegible».
Si, a veces los simbolos parecen $s4test.... Eso no es motivo para rendirse. En Swift hay una forma directa de volverlos legibles mediante swift demangle, y esta tecnica se recomienda cuando la pila contiene nombres mangled.
Error n. 5: no distinguir entre un crash/trap y un error manejado.
Si tienes throws y llegas a catch, eso no es un fallo, sino un escenario normal de manejo. Tambien es util mirar el call stack en un breakpoint, pero el sentido es distinto: averiguas de donde vino el error, no por que el proceso termino. Si mezclas estos dos mundos, puedes empezar a «tratar» errores manejados como crashes — por ejemplo, anadiendo ! donde deberia haber un guard.
GO TO FULL VERSION