Validación de inputs y errores controlados

r
shiny
Cómo evitar pantallazos rojos al usuario. req() para ‘todavía no’, validate() + need() para mensajes específicos, showNotification y modalDialog para feedback, y safeError para sanitizar errores en producción.

Por qué validar

Una app Shiny sin validación parece funcionar mientras el usuario hace lo esperado. En cuanto haga cualquier otra cosa, ves la marca de un dashboard amateur:

  • Caja roja con un error críptico en R.
  • Output vacío sin explicación.
  • App congelada esperando un cálculo imposible.
  • “Error in if (…): missing value where TRUE/FALSE needed” visible al usuario final.

Las apps en producción tienen tres niveles de validación:

  1. req(): “todavía no, no hagas nada”. Para esperas naturales (input no rellenado, fichero no subido).
  2. validate() + need(): “no se puede calcular, te explico por qué”. Mensaje de error claro al usuario en el sitio del output.
  3. showNotification() / modalDialog(): comunicación activa al usuario sobre eventos (éxito, error, confirmación).

Más una cuarta capa de producción:

  1. safeError(): sanitizar errores técnicos para que no expongan trazas internas a usuarios finales.

req(): “todavía no, no hagas nada”

req() es el caso más simple. Si el argumento es falsy (NULL, FALSE, "", 0, vector vacío), detiene silenciosamente la ejecución del reactivo:

server <- function(input, output, session) {
  output$resultado <- renderText({
    req(input$nombre)            # si input$nombre es "" o NULL, no continúa
    paste("Hola,", input$nombre)
  })
}

Sin req(), al cargar la app input$nombre es "" (vacío), paste() se ejecuta y muestra “Hola,”, feo. Con req(), simplemente no se renderiza nada hasta que haya valor.

Para múltiples requisitos:

req(input$archivo, input$variable, input$umbral)

Si cualquiera es falsy, detiene. Equivalente a req(input$archivo); req(input$variable); req(input$umbral).

Para condiciones que NO son falsy pero tampoco quieres procesar:

req(input$n >= 10)               # solo continúa si n >= 10
req(nrow(datos()) > 0)           # solo si hay filas
req(input$tipo %in% c("a", "b")) # solo si el tipo es uno de esos

req() evalúa el argumento. Si es TRUE, continúa.

Regla mental: req() es para “no es un error, simplemente no es el momento”. Si lo es, usa validate().

req(cancelOutput = TRUE)

Por defecto, req() deja el output vacío. Si quieres preservar el output anterior (no resetear), usa:

req(input$nombre, cancelOutput = TRUE)

Esto detiene la ejecución pero mantiene el output como estaba. Útil para evitar parpadeos cuando el usuario está cambiando inputs rápidamente.

validate() + need(): mensajes específicos

Cuando hay un problema real que requiere comunicar al usuario, req() no basta, necesitas explicar:

output$grafico <- renderPlot({
  validate(
    need(input$archivo, "Sube un CSV para continuar."),
    need(nrow(datos()) > 0, "El archivo está vacío."),
    need("ventas" %in% names(datos()), "El archivo debe tener una columna 'ventas'.")
  )

  ggplot(datos(), aes(fecha, ventas)) + geom_line()
})

Donde iría el gráfico, aparece el mensaje correspondiente. Sin caja roja, sin traza de error, solo el texto que tú especificaste.

need() evalúa una condición y devuelve NULL si pasa o un mensaje si falla. validate() ejecuta todos los need() y para en el primero que falle, mostrando el mensaje.

Orden importa: el primer need() que falla es el que se muestra. Ordena de más general a más específico.

showNotification(): feedback no bloqueante

Cuando ocurre algo y quieres avisar al usuario sin pararle (la operación terminó, hay un warning, se guardaron cambios):

observeEvent(input$guardar, {
  guardar_datos()
  showNotification("Cambios guardados correctamente", type = "message", duration = 3)
})

Tipos disponibles:

  • "default": gris neutral.
  • "message": azul (info).
  • "warning": amarillo.
  • "error": rojo.

Argumentos útiles:

showNotification(
  "Operación completada",
  duration = 5,      # segundos antes de desaparecer; NULL = persistente hasta cerrar
  closeButton = TRUE,
  type = "message",
  id = "mi_notif"    # con id, llamadas siguientes reemplazan la notificación
)

# Para quitarla programáticamente:
removeNotification("mi_notif")

Es la forma profesional de comunicar eventos. Beats un “se guardaron los cambios” en mitad de la pantalla.

modalDialog(): confirmaciones bloqueantes

Cuando la acción del usuario requiere confirmación explícita (borrar datos, salir sin guardar, sobrescribir un archivo):

observeEvent(input$borrar_todo, {
  showModal(modalDialog(
    title = "¿Borrar todos los datos?",
    "Esta operación no se puede deshacer.",
    footer = tagList(
      modalButton("Cancelar"),
      actionButton("confirmar_borrado", "Borrar", class = "btn-danger")
    ),
    easyClose = FALSE
  ))
})

observeEvent(input$confirmar_borrado, {
  borrar_datos()
  removeModal()
  showNotification("Datos borrados", type = "warning")
})

Patrón típico: un actionButton lanza el modal con showModal(), otro actionButton dentro del modal confirma con observeEvent(), removeModal() lo cierra.

easyClose = FALSE impide cerrar haciendo click fuera del modal, fuerza al usuario a elegir explícitamente.

safeError(): sanitizar errores en producción

En desarrollo, ver el error completo en la app ayuda a depurar. En producción, exponer el error técnico al usuario es problema:

  • Filtra rutas internas (Error in /home/server/secret-file.R...).
  • Confunde a usuarios no técnicos.
  • Revela detalles que un atacante podría aprovechar.

safeError() envuelve un error R y devuelve solo el mensaje que tú definas:

server <- function(input, output, session) {
  output$resultado <- renderTable({
    tryCatch(
      operacion_que_puede_fallar(input$datos),
      error = function(e) {
        stop(safeError("No se pudo procesar el archivo. Verifica que sea un CSV válido."))
      }
    )
  })
}

En la app aparece solo el mensaje sanitizado. El error técnico real va al log del servidor, donde lo puedes depurar sin exponerlo al usuario.

Para activar el modo “sanitizar todos los errores no-safeError a nivel global de la app:

options(shiny.sanitize.errors = TRUE)

Cualquier stop() que no sea safeError mostrará el mensaje genérico “An error has occurred”. Útil en producción. Molesto en desarrollo.

Patrón completo de validación

Un ejemplo que combina las cuatro capas:

ui <- fluidPage(
  fileInput("archivo", "Sube un CSV:"),
  numericInput("umbral", "Umbral:", value = 0.5, min = 0, max = 1),
  actionButton("procesar", "Procesar", class = "btn-primary"),
  plotOutput("grafico")
)

server <- function(input, output, session) {
  # 1. Reactive con req() para parar si no hay fichero
  datos <- reactive({
    req(input$archivo)
    tryCatch(
      read.csv(input$archivo$datapath),
      error = function(e) {
        showNotification("Error leyendo el archivo: ¿es un CSV válido?",
                         type = "error", duration = 8)
        NULL
      }
    )
  })

  # 2. observeEvent con confirmación + notificación
  observeEvent(input$procesar, {
    df <- datos()
    if (is.null(df) || nrow(df) == 0) {
      showNotification("Sube un archivo válido primero", type = "warning")
      return()
    }

    showModal(modalDialog(
      title = "Confirmar procesado",
      paste("Vas a procesar", nrow(df), "filas con umbral", input$umbral, "."),
      footer = tagList(
        modalButton("Cancelar"),
        actionButton("confirmar", "Continuar", class = "btn-primary")
      )
    ))
  })

  # 3. Output con validate() para mensajes en el sitio del gráfico
  output$grafico <- renderPlot({
    validate(
      need(datos(), "Sube un archivo CSV para empezar."),
      need(input$umbral > 0, "El umbral debe ser positivo."),
      need(nrow(datos()) > 10, "Necesitas al menos 10 filas para visualizar.")
    )

    hist(datos()[[1]], main = "Distribución", col = "steelblue")
  })
}

Cuatro mecanismos en una sola app:

  • req(): silencioso al inicio.
  • validate() + need(): mensajes específicos en el output.
  • showNotification(): feedback de eventos.
  • modalDialog(): confirmación de acciones críticas.

Trampas habituales

  • Usar validate() cuando req() basta. “No hay archivo todavía” no es un error, es estado inicial. Mostrar el mensaje “Sube un archivo” en lugar del output vacío es opcional. Si no aporta, req() directo es más limpio.
  • stop() directo en código de server. El error R aparece como caja roja al usuario. Para errores controlados, usa validate(need(...)). Para errores técnicos sanitizados, safeError. Nunca stop("...") directo en producción.
  • showNotification sin id cuando se llama repetidamente. Sin id, cada notificación se apila, el usuario ve cinco “Guardado correctamente” al hacer click rápido. Con id, la siguiente reemplaza la anterior.
  • modalDialog con easyClose = TRUE para acciones destructivas. Si el usuario puede cerrar el modal accidentalmente (click fuera), la confirmación pierde sentido. Para acciones irreversibles, easyClose = FALSE.

En la siguiente entrega

Has aprendido a validar y comunicar con el usuario. La siguiente entrega es la más importante del bloque 3: modules. Cuando tu app pasa de 200 líneas, la única forma de no perder la cabeza es modularizar. Lo siguiente.