Saltar al contenido

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.

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.

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

TypeScript
const { data, error } = await actions.crearTarea(entrada);if (error) {  // aqui data es undefined; TypeScript lo sabe  return;}// aqui, tras el early return, data esta garantizadoconsole.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.

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

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.

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.

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