wandres.dev
ACTIONS · funciones de servidor tipadas

Llamar una action: data, error e isInputError

Invocar una action desde el cliente con actions.miAccion y desgranar el resultado que nunca lanza: la unión discriminada data/error, el uso de isInputError para leer los fallos de validación campo a campo, isActionError para ramificar por código, y las variantes orThrow y Astro.callAction para el servidor.

⏱ 15 min

Definir una action es media historia; la otra media es llamarla bien. Desde el cliente, una action se invoca a través del objeto actions y devuelve siempre la misma forma: { data, error }, una pareja donde exactamente uno de los dos tiene valor. Esa forma no es un capricho estético, sino una decisión de diseño que convierte el fallo en un valor que se inspecciona, no en una excepción que se escapa. Aprender a llamar una action es, sobre todo, aprender a leer ese resultado: distinguir un dato correcto de un error de validación, y un error de validación de un fallo del servidor.

🎯 Al terminar esta lección sabrás
  • Invocar una action desde el cliente con actions.miAccion y desestructurar { data, error }.
  • Comprobar error antes de usar data gracias a la unión discriminada.
  • Usar isInputError para leer los fallos de validación campo a campo.
  • Conocer isActionError, la variante orThrow y Astro.callAction en el servidor.

actions.miAccion: la llamada tipada

La llamada nace de un import de astro:actions y se ejecuta con un await. El objeto actions es un proxy que Astro generó a partir de tu fichero server, así que cada action aparece como un método con su nombre, sus argumentos tipados y su retorno inferido. Puedes llamarla desde un <script> de una página, desde un módulo del cliente o desde una isla de framework: el mecanismo es el mismo en todos.

<script>
  import { actions } from 'astro:actions';

  const form = document.querySelector('form');
  form?.addEventListener('submit', async (e) => {
    e.preventDefault();
    const { data, error } = await actions.crearTarea({
      titulo: 'Comprar pan',
      prioridad: 'alta',
    });
    if (error) return mostrarError(error);
    pintarTarea(data);
  });
</script>

Lo primero que hay que interiorizar es lo que no ocurre: la llamada no lanza cuando la action falla por una razón esperada —una validación incumplida, una regla de negocio rota, una autorización denegada—. En vez de eso, rellena el error del resultado. Solo un fallo verdaderamente excepcional —la red caída, el servidor muerto— produce una excepción real. Esta distinción es el corazón del modelo: los fallos previsibles son datos; los imprevisibles, excepciones.

Ese mecanismo idéntico en todas partes tiene una consecuencia cómoda: una isla de framework —React, Vue, Svelte— llama a la action con el mismo import y la misma sintaxis que un <script> suelto. No hay un cliente especial por tecnología; el objeto actions es uno solo, y cualquier código del navegador que pueda hacer un await puede invocar tu backend tipado. Eso convierte a las actions en el punto de encuentro natural entre tus islas y tu servidor.

data y error: el resultado que no lanza

El retorno de una action es una unión discriminada: o bien data trae el resultado y error es undefined, o bien error describe el fallo y data es undefined. Nunca ambos, nunca ninguno. Esa forma tiene una consecuencia práctica que el compilador aprovecha: mientras no descartes el error, TypeScript no te deja usar data, porque sabe que podría ser undefined. El propio tipo te empuja a comprobar el fallo primero.

const { data, error } = await actions.crearTarea(entrada);
if (error) {
  // aqui data es undefined; TypeScript lo sabe
  return;
}
// aqui, tras el early return, data esta garantizado
console.log(data.id);

El patrón idiomático es el early return: compruebas error, y si existe, sales o ramificas; a partir de esa línea, el compilador estrecha el tipo y data queda garantizado. Es la misma disciplina que impone la lección de las redirecciones con el return, trasladada al consumo de datos: la forma del valor te obliga a tratar el caso malo antes de disfrutar del bueno. Ese error, cuando existe, es un objeto ActionError con al menos dos campos que nos importan: un code legible —como BAD_REQUEST o UNAUTHORIZED— y un message con el detalle.

Queda un caso que la pareja data y error no cubre y conviene no olvidar: el fallo catastrófico. Si la red se cae o el servidor no responde, no hay error que rellenar porque la petición ni siquiera llegó a completarse, y la llamada lanza una excepción de verdad. Por eso, en código de producción, envolver la invocación en un try/catch externo sigue teniendo sentido: el error del resultado captura los fallos que el servidor supo comunicar; el catch, los que impidieron toda comunicación. Son dos redes a distinta altura para dos clases distintas de caída.

isInputError y el detalle por campo

No todos los errores son iguales, y el más frecuente merece un trato especial: el de validación. Cuando el input de la action rechaza los datos, el error que llega es un error de entrada, y Astro ofrece isInputError para reconocerlo. Su virtud es que, cuando devuelve verdadero, el error trae un objeto fields con los mensajes agrupados por nombre de campo, justo lo que necesitas para pintar el fallo junto a cada casilla del formulario.

import { actions, isInputError } from 'astro:actions';

const { data, error } = await actions.suscribir(entrada);
if (error) {
  if (isInputError(error)) {
    // error.fields.email es un array de mensajes para ese campo
    for (const campo in error.fields) {
      marcarCampo(campo, error.fields[campo]);
    }
    return;
  }
  // no es de validacion: un fallo de otra clase
  mostrarAviso(error.message);
}

isInputError te permite separar dos experiencias muy distintas de usuario. Un fallo de validación es corregible: el usuario escribió mal un correo y basta con señalárselo bajo el campo para que lo arregle. Un fallo de otra clase —no autorizado, recurso inexistente, error interno— no se resuelve reescribiendo el formulario, y pide un mensaje general, no una marca por campo. Sin isInputError, ambos casos se mezclarían en un error.message genérico; con él, la interfaz responde a cada uno como merece.

flowchart TD
C[await actions miAccion input] --> R{error definido}
R -->|no| D[usar data ya tipado]
R -->|si| T{que clase de error}
T -->|isInputError| F[pintar error fields por campo]
T -->|isActionError| K[ramificar segun error code]
T -->|inesperado| G[fallo de red o servidor]
style D fill:#a6e3a1,color:#11111b
style F fill:#f9e2af,color:#11111b
style K fill:#f9e2af,color:#11111b
style G fill:#f38ba8,color:#11111b

Su pariente es isActionError, que reconoce un error lanzado a propósito dentro del handler con un código concreto. Sirve para estrechar el tipo de un error genérico y ramificar según su code: tratar un UNAUTHORIZED distinto de un CONFLICT. Y si prefieres el estilo de excepciones al de valores, cada action ofrece un método orThrow: actions.crearTarea.orThrow(entrada) devuelve el data directamente o lanza el error, útil cuando quieres que un try/catch de más arriba se ocupe del fallo.

💡
orThrow cuando el fallo debe subir

El modelo { data, error } es el idóneo cuando quieres manejar el fallo aquí mismo, junto a la llamada. Pero a veces el fallo debe propagarse hacia arriba —a un límite de error, a un manejador central— y comprobar error en cada llamada solo estorba. Para esos casos, orThrow invierte el contrato: devuelve el dato pelado y convierte el error en una excepción. No es mejor ni peor que la forma segura; es la herramienta para cuando el sitio correcto para tratar el fallo no es la línea de la llamada.

🎁

data y error

La union discriminada: uno tiene valor y el otro es undefined. Comprueba el error primero.

🔎

isInputError

Reconoce un fallo de validacion y expone fields con mensajes por campo.

🚦

isActionError

Estrecha un error lanzado a proposito y te deja ramificar por su code.

🎯

orThrow

Devuelve el dato pelado y convierte el fallo en excepcion cuando debe subir.

Llamar desde el servidor con Astro.callAction

El cliente no tiene el monopolio de las actions. Dentro de un componente .astro, un endpoint o el middleware, Astro.callAction invoca la misma action sin pasar por la red, ejecutando el handler en el sitio. Recibe la action y su entrada, y devuelve el mismo { data, error } que verías en el cliente.

---
import { actions } from 'astro:actions';

const { data, error } = await Astro.callAction(actions.crearTarea, {
  titulo: 'Tarea creada en el servidor',
});
---
{error ? <p>Algo fallo</p> : <p>Creada: {data.titulo}</p>}

Esta simetría es lo que hace de la action una pieza de lógica reutilizable y no un mero atajo del cliente. La regla del input se aplica igual, el resultado tiene la misma forma, y el handler sigue siendo la única fuente de verdad. Da igual desde qué orilla del cable la llames: la action se comporta idéntico, y esa uniformidad es la que te deja mover una operación del cliente al servidor sin reescribir cómo se consume.

Hay un matiz de contexto que conviene conocer: dentro de un handler, el context que recibes no incluye ni callAction ni getActionResult, para impedir que una action se invoque a sí misma en cadena sin control. Astro.callAction es, por tanto, una herramienta de páginas, endpoints y middleware, no de las propias actions. Es una restricción sana que empuja a compartir la lógica común como funciones normales que varias actions importan, en lugar de encadenar llamadas entre ellas.

El error como valor, no como excepción

Que una action devuelva { data, error } en lugar de lanzar es una de esas decisiones pequeñas en apariencia y enormes en consecuencia. Durante décadas, el manejo de errores se dividió en dos escuelas enfrentadas: los que lanzan excepciones, cómodas de escribir pero invisibles en la firma de una función —nada en el tipo te avisa de que ese await puede estallar—, y los que devuelven el error como un valor, más verbosos pero honestos, porque el fallo aparece en el tipo y el compilador te obliga a mirarlo. Las actions eligen la segunda escuela para los fallos esperados, y esa elección reconfigura cómo piensas el código del cliente. Un error de validación, uno de autorización, una regla de negocio incumplida: nada de eso es excepcional en una aplicación real, todo ello ocurre a diario y forma parte del flujo normal. Modelarlos como excepciones es fingir que son accidentes; modelarlos como valores es admitir que son estados legítimos que la interfaz debe saber pintar. Cuando el resultado es una unión discriminada, el sistema de tipos se vuelve tu aliado: no puedes olvidar el error porque no puedes tocar el dato sin antes descartarlo, y el if (error) deja de ser una cortesía que a veces se olvida para convertirse en un peaje que el compilador cobra siempre. La excepción se reserva, entonces, para lo que de verdad es excepcional —la red que se cae, el servidor que muere—, cosas ante las que no hay una rama sensata de interfaz, solo un límite de error que atrapa lo imprevisto. Aprender a llamar una action es, en el fondo, aprender esta distinción: separar el fallo que tu programa contempla del accidente que solo puede sufrir, y darle a cada uno la forma que le corresponde. El resultado es un cliente donde los caminos de error no son un añadido nervioso al final, sino parte de la estructura, tan tipada y tan visible como el camino feliz.

⚔️ Lee el resultado con criterio
  1. Llama a una action desde un <script> y desestructura { data, error }; comprueba que el editor no te deja usar data hasta que descartas error con un early return.
  2. Provoca un fallo de validación y usa isInputError para leer error.fields y marcar el campo culpable en el formulario.
  3. Lanza un error con código desde el handler y distínguelo del de validación usando isActionError y su code.
  4. Reescribe una de las llamadas con orThrow y razona en qué situación prefieres que el fallo suba como excepción en lugar de manejarlo en el sitio.