Cómo leer mensajes de error y stack traces

Trazas reales de Python, PHP, JS, Java, Dart, Swift y C leídas línea por línea: cuál es tu frame, qué significa Caused by y cómo buscar un error.

Serie: curso de pruebas y depuración 1/6 (principiante). Para quien se queda en blanco cuando aparece un muro de texto rojo en la terminal. Al terminar vas a encontrar la única línea que importa en un error de Python, PHP, JavaScript, Java, Dart, Swift o C, y a buscarla de forma que encuentres respuestas.

Si estás en un bootcamp, en tu primer trabajo, o haciendo un freelance donde el cliente te manda una captura con "me salió esto", esta es la habilidad que necesitas antes que cualquier framework de pruebas. Un stack trace (traza de pila) no es ruido: es el programa diciéndote exactamente dónde estaba cuando se rindió. El problema es que cada lenguaje lo imprime en otro orden, con otras palabras para decir "este error lo causó aquel", y con muchas líneas que no tienen nada que ver con tu código.

Todo lo que sigue se ejecutó de verdad en una Mac con Apple Silicon (M1 Pro): Python 3.9.6, PHP 8.5.6, JavaScriptCore (jsc, el motor que trae macOS), OpenJDK 26, Dart 3.11.5, Swift 6.4.2 y Apple clang 21. Las rutas de la carpeta personal se acortaron a ~/demo/; cualquier otro recorte está marcado con ….

Tu primer traceback en cinco minutos

Un stack trace (Python lo llama traceback) es la lista de llamadas a funciones que estaban activas cuando ocurrió el error. Cada entrada es un frame (marco): una función, un archivo, una línea. Si main llamó a order_total, que llamó a line_total, que falló, hay tres frames.

Guarda esto como orders.py, junto a un order.json donde el segundo producto no tiene qty (cantidad):

import json
import sys


def load(path):
    with open(path) as f:
        return json.load(f)


def line_total(item):
    return item["price"] * item["qty"]


def order_total(order):
    return sum(line_total(item) for item in order["items"])


def main(path):
    order = load(path)
    print(order["id"], order_total(order))


main(sys.argv[1])
{"id": "P-4001", "items": [{"name": "lápiz", "price": 120, "qty": 3}, {"name": "cuaderno", "price": 300}]}
$ python3 orders.py order.json
Traceback (most recent call last):
  File "~/demo/orders.py", line 23, in <module>
    main(sys.argv[1])
  File "~/demo/orders.py", line 20, in main
    print(order["id"], order_total(order))
  File "~/demo/orders.py", line 15, in order_total
    return sum(line_total(item) for item in order["items"])
  File "~/demo/orders.py", line 15, in <genexpr>
    return sum(line_total(item) for item in order["items"])
  File "~/demo/orders.py", line 11, in line_total
    return item["price"] * item["qty"]
KeyError: 'qty'

Léelo en este orden:

  1. La última línea es el tipo de error y el mensaje: KeyError: 'qty'. Un KeyError significa "este diccionario no tiene esa clave". El mensaje es la clave que falta.
  2. El encabezado dice most recent call last (la llamada más reciente, al final). Entonces el frame justo encima del error es donde falló: line_total, línea 11.
  3. Sube solo si necesitas saber cómo llegaste ahí: order_total la llamó desde una expresión generadora (<genexpr>), main llamó a eso, y el nivel de módulo llamó a main.

El arreglo no está en line_total. El código funciona con pedidos válidos; los datos no lo son. Rechazar el pedido, suponer qty = 1 o corregir a quien generó el JSON es una decisión de negocio (o del cliente, si eres freelance). El trabajo del traceback es señalar la línea correcta y la clave correcta, y lo hizo.

Diagrama de flujo en dos filas. Paso 1, leer la última línea para ver el tipo de error y el mensaje. Paso 2, encontrar el frame más reciente que está en tu propio archivo. Una caja de decisión pregunta si el error está encadenado, lo que se ve por palabras como cause, Caused by o Next. Si es así, el paso 3 lee el bloque de la causa raíz. Los dos caminos llevan al paso 4, revisar los datos en esa línea, y luego al paso 5, buscar el mensaje sin rutas ni ids.
El orden de lectura que funciona en cualquier lenguaje: la última línea, el frame más reciente en tu propio archivo, la causa raíz si el error está encadenado, los datos y, al final, la búsqueda.

Cuando el frame que falla no es tu código

Ahora rompe el JSON (una coma de más después del último elemento) y el traceback crece:

$ python3 orders.py bad.json
Traceback (most recent call last):
  File "~/demo/orders.py", line 23, in <module>
    main(sys.argv[1])
  File "~/demo/orders.py", line 19, in main
    order = load(path)
  File "~/demo/orders.py", line 7, in load
    return json.load(f)
  File "…/lib/python3.9/json/__init__.py", line 293, in load
    return loads(fp.read(),
  File "…/lib/python3.9/json/__init__.py", line 346, in loads
    return _default_decoder.decode(s)
  File "…/lib/python3.9/json/decoder.py", line 337, in decode
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
  File "…/lib/python3.9/json/decoder.py", line 355, in raw_decode
    raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 70 (char 69)

Los cuatro frames de abajo están dentro de la biblioteca estándar. Casi nunca están mal. El que importa es el frame más reciente en tu propio archivo: orders.py, línea 7, json.load(f). Y el mensaje te dice dónde mirar en la entrada.

Un detalle que apareció gracias a la tilde: con pen en vez de lápiz, el mismo JSON daba column 68. Con lápiz da column 70, dos más, porque lápiz tiene 5 caracteres y pen 3. Python cuenta caracteres, no bytes (en UTF-8 la á ocupa 2 bytes, y sin embargo cuenta como uno). Es la misma unidad que usa tu editor, así que puedes saltar directo a esa columna.

La regla que funciona en todos lados: salta los frames de la biblioteca estándar, del framework y del runtime hasta llegar a una ruta que escribiste tú. En una app de Django o Laravel eso puede ser saltar treinta frames. El error ocurrió en su código; la causa casi siempre está en lo que tu código le pasó.

Excepciones encadenadas: "caused by", "during handling" y "Next"

El código real atrapa errores de bajo nivel y los vuelve a lanzar con contexto. Entonces aparecen dos trazas en un solo reporte, y el orden importa.

def parse_order(text):
    try:
        data = json.loads(text)
        return data["id"], data["items"][0]["qty"]
    except KeyError as e:
        raise OrderError("order P-4001 is missing a field") from e
Traceback (most recent call last):
  File "~/demo/chained.py", line 12, in parse_order
    return data["id"], data["items"][0]["qty"]
KeyError: 'qty'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "~/demo/chained.py", line 27, in <module>
    parse_order(text)
  File "~/demo/chained.py", line 14, in parse_order
    raise OrderError("order P-4001 is missing a field") from e
__main__.OrderError: order P-4001 is missing a field

raise … from e es una cadena deliberada: primero el KeyError original, después el envoltorio. Compáralo con un bug dentro del propio bloque except:

Traceback (most recent call last):
  File "~/demo/chained.py", line 20, in parse_order_sloppy
    return data["id"], data["items"][0]["qty"]
KeyError: 'qty'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "~/demo/chained.py", line 29, in <module>
    parse_order_sloppy(text)
  File "~/demo/chained.py", line 22, in parse_order_sloppy
    log.warning("bad order: %s", text)   # bug: 'log' was never defined
NameError: name 'log' is not defined

"During handling" significa dos bugs: el problema de datos y un manejador de errores que se cayó (log nunca se definió). El camino que reporta errores es el código menos probado de cualquier sistema.

Java imprime primero el envoltorio y después el original, tras Caused by::

$ java Orders.java
Exception in thread "main" java.lang.IllegalArgumentException: bad qty in order P-4001
    at Orders.loadQty(Orders.java:19)
    at Orders.main(Orders.java:28)
Caused by: java.lang.NumberFormatException: For input string: "3 "
    at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
    at java.base/java.lang.Integer.parseInt(Integer.java:527)
    at java.base/java.lang.Integer.parseInt(Integer.java:624)
    at Orders.loadQty(Orders.java:17)
    at Orders.main(Orders.java:28)
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
    … 4 frames más del lanzador

La respuesta está entre comillas: "3 " tiene un espacio al final, y Integer.parseInt no recorta espacios. Pasa mucho con datos copiados de una hoja de cálculo que te mandó un cliente. En una traza larga de Java, baja hasta el último Caused by:: esa es la raíz.

PHP usa la palabra Next, con el original primero:

$ php -d display_errors=0 orders.php bad
PHP Fatal error:  Uncaught JsonException: Syntax error in ~/demo/orders.php:18
Stack trace:
#0 ~/demo/orders.php(18): json_decode('{"id":"P-4002",...', true, 512, 4194304)
#1 ~/demo/orders.php(28): loadOrder('{"id":"P-4002",...')
#2 {main}

Next RuntimeException: cannot parse order in ~/demo/orders.php:20
Stack trace:
#0 ~/demo/orders.php(28): loadOrder('{"id":"P-4002",...')
#1 {main}
  thrown in ~/demo/orders.php on line 20

PHP muestra los argumentos de cada llamada, cortados a 15 caracteres. Muchas veces eso basta para ver la entrada mala sin depurador. Usamos -d display_errors=0 porque, si no, el PHP de línea de comandos imprime cada error fatal dos veces: una por stderr a través del log y otra por stdout.

El mismo bug en siete runtimes

Así se ve el bug de "falta qty" en otros lenguajes. Fíjate en los que ni siquiera se caen.

PHP 8.5 trata una clave inexistente como una advertencia (warning) y sigue con null:

$ php -d display_errors=0 orders.php missing
PHP Warning:  Undefined array key "qty" in ~/demo/orders.php on line 5
0

El total es 0, el código de salida es 0, y el bug llega a producción. En desarrollo, trata los warnings de PHP como errores.

JavaScript (jsc) produce NaN en silencio con el campo que falta, y solo se cae cuando falta un objeto:

$ jsc orders.js
P-4001 total: NaN
Exception: TypeError: undefined is not an object (evaluating 'order.items.reduce')
orderTotal@orders.js:5:21
global code@orders.js:10:34

El formato es función@archivo:línea:columna, lo más reciente arriba. La frase undefined is not an object es de JavaScriptCore (Safari). V8, en Chrome y Node, dice Cannot read properties of undefined (reading 'reduce') para el mismo bug (no ejecutado aquí). Cuando lanzamos new Error("…", { cause: e }), la salida de jsc para el error no atrapado no imprimió la causa; tuvimos que imprimir e.cause nosotros.

Java 26 nombra el null exacto en el mensaje ("helpful NullPointerExceptions"):

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.Map.get(Object)" is null
    at Orders.lineTotal(Orders.java:6)
    at Orders.orderTotal(Orders.java:11)
    at Orders.main(Orders.java:26)

Dart imprime frames numerados, lo más reciente primero, y los frames del propio runtime abajo:

Unhandled exception:
type 'Null' is not a subtype of type 'int' in type cast
#0      lineTotal (file://~/demo/orders.dart:4:48)
#1      orderTotal (file://~/demo/orders.dart:10:12)
#2      main (file://~/demo/orders.dart:17:9)
#3      _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:314:19)
#4      _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:193:12)

"Null no es un subtipo de int" es la forma de Dart de decir "convertiste a int un valor que no existe". Si haces apps con Flutter que leen JSON de una API, lo vas a ver a diario.

Swift da una sola línea y el código de salida 133 (una trampa, trap):

$ ./orders_swift
orders_swift/orders.swift:4: Fatal error: Unexpectedly found nil while unwrapping an Optional value

Para ver la pila, ejecútalo dentro de lldb y escribe bt (backtrace). Los frames 0 a 3 son el runtime de Swift reportando el error; el frame 4 es tuyo, y lldb hasta muestra el argumento que lo rompió:

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = Fatal error: Unexpectedly found nil while unwrapping an Optional value
     frame #0: … libswiftCore.dylib`_swift_runtime_on_report
     frame #1: … libswiftCore.dylib`_swift_stdlib_reportFatalErrorInFile + 204
     frame #2: … libswiftCore.dylib`closure #1 in closure #1 in _assertionFailure(_:_:file:line:flags:) + 400
     frame #3: … libswiftCore.dylib`_assertionFailure(_:_:file:line:flags:) + 276
   * frame #4: 0x0000000100000ddc orders_swift`lineTotal(name="cuaderno", qty=1) at orders.swift:4:24
     frame #5: 0x0000000100000adc orders_swift`orderTotal(items=2 values) at orders.swift:9:39
     frame #6: 0x0000000100000838 orders_swift`main at orders.swift:13:7
     frame #7: … dyld`start + 6992

C es el que da miedo. Un bucle con i <= n_items lee un elemento más allá de un bloque de 3 enteros reservado con malloc. Compilado normal, imprime 500 y sale con 0: la lectura de más justo encontró un cero. Compilado con AddressSanitizer (-fsanitize=address -g), se detiene en la primera lectura inválida:

==37032==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6020000000fc at pc 0x000104330a8c bp 0x00016bace320 sp 0x00016bace318
READ of size 4 at 0x6020000000fc thread T0
    #0 0x000104330a88 in line_total orders.c:5
    #1 0x0001043309dc in order_total orders.c:11
    #2 0x000104330964 in main orders.c:18
    #3 0x000188a244e0 in start+0x1b4c (dyld:arm64e+0x204e0)

0x6020000000fc is located 0 bytes after 12-byte region [0x6020000000f0,0x6020000000fc)
allocated by thread T0 here:
    #0 0x000104b71274 in malloc+0x78 (libclang_rt.asan_osx_dynamic.dylib:arm64e+0x41274)
    #1 0x00010433082c in main orders.c:16
    #2 0x000188a244e0 in start+0x1b4c (dyld:arm64e+0x204e0)

SUMMARY: AddressSanitizer: heap-buffer-overflow orders.c:5 in line_total
… 30 líneas del mapa de shadow bytes

El reporte completo tiene 47 líneas. Necesitas tres cosas: el tipo de error (heap-buffer-overflow), el frame #0 de la primera pila (orders.c:5) y "0 bytes after 12-byte region", que significa "leíste el elemento justo después del final de un arreglo de 3 int". La segunda pila dice dónde se reservó ese arreglo. El mapa de shadow bytes de abajo es para quienes desarrollan el sanitizer; sáltalo.

Chuleta: qué extremo es el más reciente y cómo se llama la cadena

Runtime Frame más reciente Palabra de la cadena Qué leer primero
Python abajo (most recent call last) direct cause (from) / During handling (bug en el manejador) última línea, luego el frame de encima
Java arriba Caused by: (original después del envoltorio) el último bloque Caused by:
PHP arriba (#0) Next (envoltorio después del original) primera línea: tipo, archivo:línea
JavaScript (jsc) arriba no imprime cause si no se atrapa línea Exception: y el primer fn@archivo:línea:col
Dart arriba (#0) no se imprime por defecto línea 2, luego #0
Swift arriba en bt de lldb ninguna el frame marcado con * en lldb
C + ASan arriba (#0) ninguna la línea SUMMARY: y "N bytes after"

Y los errores que vas a ver en tu primera semana, copiados de ejecuciones reales:

Runtime Error que vas a ver Causa típica
Python TypeError: can only concatenate str (not "int") to str "total: " + 3; usa un f-string
Python IndentationError: expected an indented block el cuerpo de un if/def sin sangría
Python ModuleNotFoundError: No module named 'reqests' error de tipeo o entorno virtual equivocado
PHP Warning: Undefined variable $count error de tipeo; el script sigue corriendo
PHP Uncaught Error: Call to undefined function tota() error de tipeo o falta un require
PHP TypeError: applyCoupon(): Argument #2 ($percent) must be of type int, string given con strict_types, los valores de un formulario siempre son strings
JS (jsc) ReferenceError: Can't find variable: totl error de tipeo; V8 dice totl is not defined (no ejecutado)
JS (jsc) TypeError: null is not an object (evaluating 'null.length') un valor que no llegó de la API
Java ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3 <= en vez de <
Java error: cannot find symbol error de compilación: tipeo o falta un import
Dart RangeError (length): Invalid value: Not in inclusive range 0..2: 3 error por uno (off-by-one)
Dart Null check operator used on a null value un ! sobre una clave de mapa que no existe
Swift Fatal error: Index out of range error por uno; código de salida 133
C código de salida 139, sin mensaje segmentation fault; recompila con -fsanitize=address -g
C error: call to undeclared function 'totl' -Wimplicit-function-declaration; falta un #include o hay un tipeo
Gráfico de barras de frames impresos. Java con cadena Caused by: 12 frames, 4 en tu archivo. Swift con bt de lldb: 8 frames, 3 tuyos. Python con JSONDecodeError: 7 frames, 3 tuyos. C con AddressSanitizer, pilas de acceso y de reserva: 7 frames, 4 tuyos. PHP con cadena Next: 5 frames, los 5 en tu archivo. Dart: 5 frames, 3 tuyos. JavaScriptCore: 2 frames, ambos tuyos.
Frames de pila que imprimió cada runtime en los ejemplos de este artículo, y cuántos apuntan a tu propio archivo (Mac con Apple Silicon M1 Pro, una ejecución cada uno; el número de frames es determinista).

Error común: imprimir solo el mensaje

El error. Atrapas la excepción y la imprimes:

try:
    total = order["items"][0]["price"] * order["items"][0]["qty"]
except Exception as e:
    print("error:", e)                 # lo que escribe quien empieza
    print("repr: ", repr(e))           # mejor: muestra el tipo
    traceback.print_exc()              # lo mejor: toda la historia

Lo que pasa de verdad.

error: 'qty'
repr:  KeyError('qty')
Traceback (most recent call last):
  File "~/demo/swallow.py", line 5, in <module>
    total = order["items"][0]["price"] * order["items"][0]["qty"]
KeyError: 'qty'

str(e) de un KeyError es solo 'qty'. En un log lleno de líneas como error: 'qty', nadie sabe qué tipo de error fue ni de dónde vino. El arreglo: registra el tipo y la pila, con traceback.print_exc() o, en código real, logger.exception(...) (la clase 2 lo muestra). Vale para todos los lenguajes: en Java e.printStackTrace() o un logger con la excepción como último argumento, en PHP (string) $e, y en JavaScript e.stack.

Otro clásico: abrir json/decoder.py e intentar arreglar la biblioteca estándar. Si el frame más reciente está en código que no escribiste, sube hasta el frame más reciente que sí escribiste.

Cómo buscar bien un mensaje de error

Los buscadores y los gestores de issues comparan palabras. Tu línea de error tiene palabras que nadie más tiene: tu carpeta personal, tu número de pedido, una dirección de memoria, un id de proceso. Quítalas. Este filtro pequeño lo hace con los ejemplos de arriba:

import re
import sys

RULES = [
    (r"(?:[A-Za-z]:)?(?:/[\w.@~-]+)+/", ""),        # directories -> keep file name only
    (r"0x[0-9a-fA-F]+", ""),                          # addresses
    (r"==\d+==", ""),                                 # sanitizer PIDs
    (r"'[^']{12,}'|\"[^\"]{12,}\"", "'…'"),          # long quoted user data
    (r"\b[A-Z]-\d+\b|\b\d{4,}\b", ""),                # ids, big numbers
    (r"\s+", " "),
]


def error_key(line):
    for pattern, repl in RULES:
        line = re.sub(pattern, repl, line)
    return line.strip()


for raw in sys.stdin:
    if raw.strip():
        print(error_key(raw))
$ python3 errkey.py < samples.txt
FileNotFoundError: [Errno 2] No such file or directory: '…'
PHP Fatal error: Uncaught JsonException: Syntax error in orders.php:18
java.lang.IllegalArgumentException: bad qty in order
ERROR: AddressSanitizer: heap-buffer-overflow on address at pc bp sp

Después busca entre comillas la parte estable, agrega el nombre del lenguaje o de la biblioteca y, si el comportamiento cambió hace poco, su versión: "Unexpectedly found nil while unwrapping an Optional value" swift dictionary. Busca el mensaje en inglés tal cual, sin traducirlo: hay muchas más respuestas. Si el mensaje lo produjo tu propio código (bad qty in order), buscar en la web no sirve: busca el texto en tu repositorio, y llegarás directo al throw.

Límites honestos y notas de versión

  • Python 3.9 es el que viene con las herramientas de línea de comandos de Xcode en esta Mac. Python 3.11 agrega marcas ^^^^ bajo la subexpresión exacta que falló, y 3.10 agrega sugerencias "Did you mean …?" a NameError y AttributeError. No ejecutamos ninguna de las dos.
  • Los mensajes detallados de NullPointerException de Java llegaron en JDK 14 (JEP 358) y quedaron activados por defecto en JDK 15; los comprobamos en 26. Nuestro bloque Caused by mostró frames del lanzador porque usamos java Orders.java de un solo archivo; una app compilada imprime ... N more.
  • La opción cause de Error en JavaScript es de ES2022. Node y los navegadores imprimen las causas de otra forma que jsc; solo ejecutamos jsc.
  • El backtracer integrado de Swift se negó a funcionar en nuestro entorno ("backtrace-on-crash is not supported for privileged executables"), así que usamos lldb. En Linux viene activado por defecto.
  • El número de líneas y las direcciones de ASan cambian entre ejecuciones; los frames no.

Ejercicios

  1. Un traceback de Python termina con ValueError: invalid literal for int() with base 10: '3 a'. Sus últimos tres frames están en ~/project/app.py, …/site-packages/pandas/… y …/site-packages/pandas/…. ¿Dónde miras primero?
  2. Un log de Java muestra IllegalStateException: checkout failed seguido de dos bloques Caused by:: SQLException y luego SocketTimeoutException. ¿Cuál investigas?
  3. Convierte esto en una buena búsqueda: PHP Warning: Undefined array key "qty" in /var/www/shop/src/orders.php on line 5.

Respuestas

  1. El frame más reciente en app.py: es la línea que le pasó '3 a' a pandas. Pandas está reportando tus datos, no un bug suyo.
  2. El último, SocketTimeoutException: la llamada a la base de datos agotó el tiempo de espera, y todo lo de arriba son envoltorios. Después revisa por qué la red o la base de datos estaban lentas.
  3. "Undefined array key" php y, si ayuda, la versión de PHP. Quita la ruta, el número de línea y qty. Mejor aún, lee primero la línea 5: el mensaje ya te dice que falta la clave.

Siguiente: Clase 2: print, logging y el depurador · Más adelante en esta serie: tus primeras pruebas unitarias, mocks y fixtures, pruebas inestables y git bisect. Para más sobre sanitizers y lldb, mira comportamiento indefinido en C (en inglés). Vuelve a la ruta de estudio de programación.

Preguntas frecuentes

¿Un stack trace se lee de arriba hacia abajo o de abajo hacia arriba?

Depende del lenguaje. Un traceback de Python imprime la llamada más reciente al final, así que la línea que falló está abajo, justo encima del tipo de error. Java, JavaScript, Dart, PHP, lldb y AddressSanitizer imprimen la llamada más reciente primero, arriba. En los dos casos, busca el frame más reciente que esté en tu propio archivo.

¿Qué significa 'Caused by' en un stack trace de Java?

Que alguien atrapó una excepción y lanzó una nueva con la original adjunta como causa. El primer bloque es el envoltorio; el bloque Caused by es el problema original y casi siempre el que hay que arreglar. Python muestra la misma idea con 'The above exception was the direct cause of the following exception', con la original primero, y PHP imprime el envoltorio después de la palabra Next.

¿Cómo busco un mensaje de error en la web?

Copia el tipo de error y la parte fija del mensaje, y borra todo lo que es propio de tu máquina: carpetas, direcciones de memoria, ids de proceso, números de pedido y valores largos entre comillas. 'Uncaught JsonException: Syntax error' encuentra respuestas; la misma línea con tu carpeta personal y el número de línea casi no encuentra nada.

  • #depuración
  • #stack trace
  • #traceback
  • #mensajes de error
  • #principiante

t.laboratory

A technical blog that records real problems from the apps, servers and services we build and run — and the code that fixed them.

Acerca de · Apps

↑