¿Cómo devuelvo la respuesta de una llamada asíncrona?

Tengo una función foo que hace una petición Ajax. Cómo puedo devolver la respuesta de foo?

He intentado devolver el valor de la llamada de retorno success así como asignar la respuesta a una variable local dentro de la función y devolverla, pero ninguna de esas formas devuelve realmente la respuesta.

function foo() {
    var result;

    $.ajax({
        url: '...',
        success: function(response) {
            result = response;
            // return response; // <- I tried that one as well
        }
    });

    return result;
}

var result = foo(); // It always ends up being `undefined`.
Solución

→ Para una explicación más general del comportamiento asíncrono con diferentes ejemplos, por favor vea https://stackoverflow.com/q/23667086/218196

*→ Si ya entiendes el problema, salta a las posibles soluciones de abajo.

El problema

La A en Ajax significa asíncrono . Eso significa que el envío de la petición (o más bien la recepción de la respuesta) se saca del flujo de ejecución normal. En tu ejemplo, $.ajax retorna inmediatamente y la siguiente sentencia, return result;, se ejecuta antes de que la función que pasaste como callback success haya sido llamada. Aquí hay una analogía que espero que aclare la diferencia entre el flujo síncrono y el asíncrono:

Sincrónico

Imagina que llamas por teléfono a un amigo y le pides que te busque algo. Aunque puede tardar un rato, esperas al teléfono y miras fijamente al espacio, hasta que tu amigo te da la respuesta que necesitabas. Lo mismo ocurre cuando haces una llamada a una función que contiene código "normal":

function findItem() {
    var item;
    while(item_not_found) {
        // search
    }
    return item;
}

var item = findItem();

// Do something with item
doSomethingElse();

Aunque findItem pueda tardar mucho en ejecutarse, cualquier código que venga después de var item = findItem(); tiene que esperar hasta que la función devuelva el resultado.

Asíncrono

Vuelves a llamar a tu amigo por el mismo motivo. Pero esta vez le dices que tienes prisa y que te llame al móvil. Cuelgas, sales de casa y haces lo que tenías pensado hacer. Una vez que tu amigo te devuelve la llamada, te ocupas de la información que te ha dado. Eso es exactamente lo que ocurre cuando haces una petición Ajax.

findItem(function(item) {
    // Do something with item
});
doSomethingElse();

En lugar de esperar la respuesta, la ejecución continúa inmediatamente y se ejecuta la declaración después de la llamada Ajax. Para obtener la respuesta eventualmente, se proporciona una función para ser llamada una vez que la respuesta fue recibida, un callback (¿notas algo? call back ?). Cualquier sentencia que venga después de esa llamada se ejecuta antes de llamar al callback.

Solución(es)

**Mientras que ciertas operaciones asíncronas proporcionan contrapartes síncronas (también lo hace "Ajax"), generalmente se desaconseja su uso, especialmente en el contexto del navegador. ¿Por qué es malo? JavaScript se ejecuta en el hilo de la interfaz de usuario del navegador y cualquier proceso de larga duración bloqueará la interfaz de usuario, haciendo que no responda. Además, hay un límite máximo en el tiempo de ejecución de JavaScript y el navegador preguntará al usuario si desea continuar la ejecución o no. Todo esto es realmente una mala experiencia para el usuario. El usuario no podrá saber si todo funciona bien o no. Además, el efecto será peor para los usuarios con una conexión lenta. A continuación veremos tres soluciones diferentes que se superponen entre sí:

  • Promesas con async/await (ES2017+, disponible en navegadores antiguos si se utiliza un transpilador o regenerador)
  • Callbacks (popular en node)
  • Promesas con then() (ES2015+, disponible en navegadores antiguos si se utiliza una de las muchas bibliotecas de promesas) Los tres están disponibles en los navegadores actuales, y en node 7+.

    ES2017+: Promesas con async/await

    La versión de ECMAScript lanzada en 2017 introdujo soporte a nivel de sintaxis para funciones asíncronas. Con la ayuda de async y await, se puede escribir asíncrono en un "estilo síncrono". El código sigue siendo asíncrono, pero es más fácil de leer/entender. async/await se basa en las promesas: una función async siempre devuelve una promesa. La función await "desenvuelve" una promesa y da como resultado el valor con el que se resolvió la promesa o lanza un error si la promesa fue rechazada. Importante: Sólo puedes usar await dentro de una función async. En este momento, el nivel superior de await aún no está soportado, por lo que es posible que tengas que hacer un IIFE asíncrono (Immediately Invoked Function Expression) para iniciar un contexto async. Puedes leer más sobre async y await en MDN. Aquí hay un ejemplo que se basa en el retraso anterior:

// Using 'superagent' which will return a promise.
var superagent = require('superagent')

// This is isn't declared as `async` because it already returns a promise
function delay() {
  // `delay` returns a promise
  return new Promise(function(resolve, reject) {
    // Only `delay` is able to resolve or reject the promise
    setTimeout(function() {
      resolve(42); // After 3 seconds, resolve the promise with value 42
    }, 3000);
  });
}

async function getAllBooks() {
  try {
    // GET a list of book IDs of the current user
    var bookIDs = await superagent.get('/user/books');
    // wait for 3 seconds (just for the sake of this example)
    await delay();
    // GET information about each book
    return await superagent.get('/books/ids='+JSON.stringify(bookIDs));
  } catch(error) {
    // If any of the awaited promises was rejected, this catch block
    // would catch the rejection reason
    return null;
  }
}

// Start an IIFE to use `await` at the top level
(async function(){
  let books = await getAllBooks();
  console.log(books);
})();

Las versiones actuales de browser y node soportan async/await. También puedes soportar entornos más antiguos transformando tu código a ES5 con la ayuda de regenerator (o herramientas que usan regenerator, como Babel).

Dejar que las funciones acepten callbacks

Una devolución de llamada es simplemente una función pasada a otra función. Esa otra función puede llamar a la función pasada siempre que esté lista. En el contexto de un proceso asíncrono, el callback será llamado cuando el proceso asíncrono haya terminado. Normalmente, el resultado se pasa a la llamada de retorno. En el ejemplo de la pregunta, puedes hacer que foo acepte una llamada de retorno y usarla como llamada de retorno de éxito. Así que esto

var result = foo();
// Code that depends on 'result'

se convierte en

foo(function(result) {
    // Code that depends on 'result'
});

Aquí definimos la función "inline" pero se puede pasar cualquier referencia a la función:

function myCallback(result) {
    // Code that depends on 'result'
}

foo(myCallback);

El propio foo se define de la siguiente manera:

function foo(callback) {
    $.ajax({
        // ...
        success: callback
    });
}

callback se referirá a la función que le pasamos a foo cuando la llamamos y simplemente se la pasamos a success. Es decir, una vez que la petición Ajax tiene éxito, $.ajax llamará a callback y pasará la respuesta a la callback (a la que se puede referir con resultado, ya que así es como definimos la callback). También se puede procesar la respuesta antes de pasarla al callback:

function foo(callback) {
    $.ajax({
        // ...
        success: function(response) {
            // For example, filter the response
            callback(filtered_response);
        }
    });
}

Es más fácil de lo que parece escribir código utilizando callbacks. Después de todo, JavaScript en el navegador está fuertemente orientado a eventos (eventos DOM). Recibir la respuesta Ajax no es más que un evento.
Podrían surgir dificultades cuando tengas que trabajar con código de terceros, pero la mayoría de los problemas se pueden resolver simplemente pensando en el flujo de la aplicación.

ES2015+: Promesas con then()

La API de promesas es una nueva característica de ECMAScript 6 (ES2015), pero ya tiene un buen soporte por parte de los navegadores. También hay muchas bibliotecas que implementan la API Promises estándar y proporcionan métodos adicionales para facilitar el uso y la composición de funciones asíncronas (por ejemplo, bluebird). Las promesas son contenedores de valores futuros. Cuando la promesa recibe el valor (es resuelta) o cuando es cancelada (rechazada), notifica a todos sus "oyentes" que quieren acceder a este valor. La ventaja sobre los callbacks simples es que permiten desacoplar el código y son más fáciles de componer. Este es un ejemplo sencillo de uso de una promesa:

function delay() {
  // `delay` returns a promise
  return new Promise(function(resolve, reject) {
    // Only `delay` is able to resolve or reject the promise
    setTimeout(function() {
      resolve(42); // After 3 seconds, resolve the promise with value 42
    }, 3000);
  });
}

delay()
  .then(function(v) { // `delay` returns a promise
    console.log(v); // Log the value once it is resolved
  })
  .catch(function(v) {
    // Or do something else if it is rejected 
    // (it would not happen in this example, since `reject` is not called).
  });

Aplicado a nuestra llamada Ajax podríamos usar promesas como esta

function ajax(url) {
  return new Promise(function(resolve, reject) {
    var xhr = new XMLHttpRequest();
    xhr.onload = function() {
      resolve(this.responseText);
    };
    xhr.onerror = reject;
    xhr.open('GET', url);
    xhr.send();
  });
}

ajax("/echo/json")
  .then(function(result) {
    // Code depending on result
  })
  .catch(function() {
    // An error occurred
  });

Describir todas las ventajas que ofrecen las promesas está más allá del alcance de esta respuesta, pero si escribes código nuevo, deberías considerarlas seriamente. Proporcionan una gran abstracción y separación de tu código. Más información sobre las promesas: HTML5 rocks - JavaScript Promises

Nota al margen: los objetos diferidos de jQuery

Los Deferred objects son la implementación personalizada de jQuery de las promesas (antes de que se estandarizara la API de promesas). Se comportan casi como las promesas pero exponen una API ligeramente diferente. Cada método Ajax de jQuery ya devuelve un "objeto diferido" (en realidad una promesa de un objeto diferido) que puedes simplemente devolver desde tu función:

function ajax() {
    return $.ajax(...);
}

ajax().done(function(result) {
    // Code depending on result
}).fail(function() {
    // An error occurred
});

Nota al margen: Promise gotchas

Tenga en cuenta que las promesas y los objetos diferidos son sólo contenedores para un valor futuro, no son el valor en sí. Por ejemplo, supongamos que tenemos lo siguiente

function checkPassword() {
    return $.ajax({
        url: '/password',
        data: {
            username: $('#username').val(),
            password: $('#password').val()
        },
        type: 'POST',
        dataType: 'json'
    });
}

if (checkPassword()) {
    // Tell the user they're logged in
}

Este código no entiende los problemas de asincronía anteriores. Específicamente, $.ajax() no congela el código mientras comprueba la página '/password' en su servidor - envía una petición al servidor y mientras espera, devuelve inmediatamente un objeto jQuery Ajax Deferred, no la respuesta del servidor. Esto significa que la sentencia if siempre va a obtener este objeto Deferred, lo tratará como true, y procederá como si el usuario estuviera conectado. Esto no es bueno. Pero la solución es fácil:

checkPassword()
.done(function(r) {
    if (r) {
        // Tell the user they're logged in
    } else {
        // Tell the user their password was bad
    }
})
.fail(function(x) {
    // Tell the user something bad happened
});

No se recomienda: Llamadas "Ajax" sincrónicas

Como he mencionado, algunas operaciones asíncronas tienen contrapartidas síncronas. No abogo por su uso, pero por si acaso, así es como se realizaría una llamada sincrónica:

Sin jQuery

Si utiliza directamente un objeto XMLHTTPRequest, pase false como tercer argumento a .open.

jQuery

Si utiliza jQuery, puede establecer la opción async a false. Tenga en cuenta que esta opción está desaparecida desde jQuery 1.8. En ese caso, puede seguir utilizando una llamada de retorno success o acceder a la propiedad responseText del objeto jqXHR:

function foo() {
    var jqXHR = $.ajax({
        //...
        async: false
    });
    return jqXHR.responseText;
}

Si utilizas cualquier otro método jQuery Ajax, como $.get, $.getJSON, etc., tienes que cambiarlo por $.ajax (ya que sólo puedes pasar parámetros de configuración a $.ajax). **No es posible hacer una petición síncrona JSONP. JSONP, por su propia naturaleza, es siempre asíncrono (una razón más para no considerar esta opción).

Comentarios (35)

Si no estás usando jQuery en tu código, esta respuesta es para ti

Tu código debería ser algo parecido a esto:

function foo() {
    var httpRequest = new XMLHttpRequest();
    httpRequest.open('GET', "/echo/json");
    httpRequest.send();
    return httpRequest.responseText;
}

var result = foo(); // always ends up being 'undefined'

Felix Kling hizo un buen trabajo escribiendo una respuesta para las personas que usan jQuery para AJAX, he decidido proporcionar una alternativa para las personas que no lo son.

(Nota, para los que usan la nueva API fetch, Angular o promises he añadido otra respuesta más abajo)


Lo que se está enfrentando

Este es un breve resumen de la "Explicación del problema" de la otra respuesta, si no estás seguro después de leer esto, lee eso.

La A en AJAX significa asíncrono. Eso significa que el envío de la petición (o más bien la recepción de la respuesta) se saca del flujo de ejecución normal. En tu ejemplo, .send devuelve inmediatamente y la siguiente sentencia, return result;, se ejecuta antes de que la función que pasaste como callback success haya sido llamada.

Esto significa que cuando está regresando, el listener que has definido no se ha ejecutado todavía, lo que significa que el valor que estás devolviendo no ha sido definido.

He aquí una simple analogía

function getFive(){ 
    var a;
    setTimeout(function(){
         a=5;
    },10);
    return a;
}

[(Fiddle)][2]

El valor de a devuelto es undefined ya que la parte a=5 no se ha ejecutado todavía. AJAX actúa así, está devolviendo el valor antes de que el servidor tenga la oportunidad de decirle al navegador cuál es ese valor.

Una posible solución a este problema es codificar re-activamente , diciéndole a tu programa qué hacer cuando el cálculo se haya completado.

function onComplete(a){ // When the code completes, do this
    alert(a);
}

function getFive(whenDone){ 
    var a;
    setTimeout(function(){
         a=5;
         whenDone(a);
    },10);
}

Esto se llama CPS. Básicamente, estamos pasando a getFive una acción a realizar cuando se completa, estamos diciendo a nuestro código cómo reaccionar cuando un evento se completa (como nuestra llamada AJAX, o en este caso el tiempo de espera).

El uso sería:

getFive(onComplete);

Lo que debería alertar "5" a la pantalla. [(Fiddle)][4].

Posibles soluciones

Hay básicamente dos formas de resolver esto:

  1. Hacer que la llamada AJAX sea sincrónica (llamémosla SJAX).
  2. Reestructurar tu código para que funcione correctamente con callbacks.

1. AJAX sincrónico - ¡No lo hagas!

En cuanto a AJAX sincrónico, no lo hagas La respuesta de Felix plantea algunos argumentos convincentes sobre por qué es una mala idea. Para resumir, congelará el navegador del usuario hasta que el servidor devuelva la respuesta y creará una muy mala experiencia de usuario. Aquí hay otro breve resumen tomado de MDN sobre el porqué:

XMLHttpRequest soporta tanto comunicaciones síncronas como asíncronas. En general, sin embargo, las peticiones asíncronas deberían ser preferidas a las síncronas por razones de rendimiento.

En resumen, las peticiones síncronas bloquean la ejecución del código... ...esto puede causar serios problemas...

Si usted tiene que hacerlo, puede pasar una bandera: Aquí está cómo:

var request = new XMLHttpRequest();
request.open('GET', 'yourURL', false);  // `false` makes the request synchronous
request.send(null);

if (request.status === 200) {// That's HTTP for 'ok'
  console.log(request.responseText);
}

2. Reestructurar el código

Deje que su función acepte una devolución de llamada. En el código de ejemplo se puede hacer que foo acepte una llamada de retorno. Le diremos a nuestro código cómo reaccionar cuando foo se complete.

Así que:

var result = foo();
// code that depends on `result` goes here

Se convierte en:

foo(function(result) {
    // code that depends on `result`
});

Aquí pasamos una función anónima, pero podríamos pasar fácilmente una referencia a una función existente, haciendo que se vea como

function myHandler(result) {
    // code that depends on `result`
}
foo(myHandler);

Para más detalles sobre cómo se hace este tipo de diseño de devolución de llamada, revisa la respuesta de Felix.

Ahora, definamos el propio foo para que actúe en consecuencia

function foo(callback) {
    var httpRequest = new XMLHttpRequest();
    httpRequest.onload = function(){ // when the request is loaded
       callback(httpRequest.responseText);// we're calling our method
    };
    httpRequest.open('GET', "/echo/json");
    httpRequest.send();
}

[(fiddle)][6]

Ahora hemos hecho que nuestra función foo acepte una acción que se ejecute cuando el AJAX se complete con éxito, podemos extender esto más allá comprobando si el estado de la respuesta no es 200 y actuando en consecuencia (crear un manejador de fallos y tal). Resolviendo efectivamente nuestro problema.

Si todavía te cuesta entender esto lee la guía de inicio de AJAX en MDN.

Comentarios (4)

XMLHttpRequest 2 (primero lee las respuestas de Benjamin Gruenbaum y Felix Kling) Si no usas jQuery y quieres un XMLHttpRequest 2 bonito y corto que funcione en los navegadores modernos y también en los móviles te sugiero que lo uses de esta manera:

function ajax(a, b, c){ // URL, callback, just a placeholder
  c = new XMLHttpRequest;
  c.open('GET', a);
  c.onload = b;
  c.send()
}

Como puedes ver:

  1. Es más corto que el resto de funciones listadas.
  2. La devolución de llamada se establece directamente (por lo que no hay cierres extra innecesarios).
  3. Utiliza el nuevo onload (por lo que no tiene que comprobar el readystate && status)
  4. Hay algunas otras situaciones que no recuerdo que hacen que el XMLHttpRequest 1 sea molesto. Hay dos formas de obtener la respuesta de esta llamada Ajax (tres usando el nombre var de XMLHttpRequest): La más sencilla:
this.response

O si por alguna razón bind() el callback a una clase:

e.target.response

Ejemplo:

function callback(e){
  console.log(this.response);
}
ajax('URL', callback);

O (la anterior es mejor las funciones anónimas son siempre un problema):

ajax('URL', function(e){console.log(this.response)});

Nada más fácil. Ahora algunas personas probablemente dirán que es mejor usar onreadystatechange o incluso el nombre de la variable XMLHttpRequest. Eso es incorrecto. Mira XMLHttpRequest características avanzadas Soporta todos los *navegadores modernos. Y puedo confirmarlo ya que estoy usando este enfoque desde que existe XMLHttpRequest 2. Nunca he tenido ningún tipo de problema en todos los navegadores que uso. onreadystatechange solo es útil si quieres obtener las cabeceras en el estado 2. Usar el nombre de la variable XMLHttpRequest es otro gran error ya que necesitas ejecutar el callback dentro de los cierres de onload/oreadystatechange sino lo pierdes.

Ahora bien, si quieres algo más complejo usando post y FormData puedes extender fácilmente esta función:

function x(a, b, e, d, c){ // URL, callback, method, formdata or {key:val},placeholder
  c = new XMLHttpRequest;
  c.open(e||'get', a);
  c.onload = b;
  c.send(d||null)
}

De nuevo... es una función muy corta, pero consigue & post. Ejemplos de uso:

x(url, callback); // By default it's get so no need to set
x(url, callback, 'post', {'key': 'val'}); // No need to set post data

O pasar un elemento de formulario completo (document.getElementsByTagName('form')[0]):

var fd = new FormData(form);
x(url, callback, 'post', fd);

O establecer algunos valores personalizados:

var fd = new FormData();
fd.append('key', 'val')
x(url, callback, 'post', fd);

Como puedes ver no he implementado la sincronización... es algo malo. Dicho esto... ¿por qué no lo haces de la manera más fácil?

Como se menciona en el comentario el uso de error && síncrono hace romper completamente el punto de la respuesta. ¿Cuál es una forma corta de usar Ajax de la manera correcta? Error handler

function x(a, b, e, d, c){ // URL, callback, method, formdata or {key:val}, placeholder
  c = new XMLHttpRequest;
  c.open(e||'get', a);
  c.onload = b;
  c.onerror = error;
  c.send(d||null)
}

function error(e){
  console.log('--Error--', this.type);
  console.log('this: ', this);
  console.log('Event: ', e)
}
function displayAjax(e){
  console.log(e, this);
}
x('WRONGURL', displayAjax);

En el script anterior, tienes un manejador de errores que está definido estáticamente para no comprometer la función. El manejador de errores puede ser usado para otras funciones también. Pero para realmente sacar un error la única manera es escribir una URL incorrecta en cuyo caso todos los navegadores arrojan un error. Los manejadores de error son tal vez útiles si se establecen cabeceras personalizadas, se establece el responseType a blob array buffer o lo que sea... Incluso si pasas 'POSTAPAPAP' como método, no arrojará un error. Incluso si pasas 'fdggdgilfdghfldj' como formdata no dará error. En el primer caso el error está dentro de displayAjax() bajo this.statusText como Method not Allowed. En el segundo caso, simplemente funciona. Tienes que comprobar en el lado del servidor si has pasado los datos correctos del post. cross-domain not allowed lanza un error automáticamente. En la respuesta de error, no hay códigos de error. Sólo está el this.type que se establece como error. ¿Por qué añadir un manejador de errores si no tienes ningún control sobre los errores? La mayoría de los errores son devueltos dentro de esto en la función callback displayAjax(). Así que: No es necesario comprobar los errores si eres capaz de copiar y pegar la URL correctamente. ;) PS: Como primera prueba escribí x('x', displayAjax)..., y totalmente obtuvo una respuesta...?? Así que comprobé la carpeta donde se encuentra el HTML, y había un archivo llamado 'x.xml'. Así que incluso si olvidas la extensión de tu archivo XMLHttpRequest 2 LO ENCONTRARÁ. Me ha hecho gracia

Leer un archivo sincrónico *No hagas eso. Si quieres bloquear el navegador durante un rato carga un bonito y gran archivo .txt de forma sincrónica.

function omg(a, c){ // URL
  c = new XMLHttpRequest;
  c.open('GET', a, true);
  c.send();
  return c; // Or c.response
}

Ahora puedes hacer

 var res = omg('thisIsGonnaBlockThePage.txt');

No hay otra forma de hacer esto de forma no asíncrona. (Sí, con el bucle setTimeout... ¿pero en serio?) Otro punto es... si trabajas con APIs o simplemente con los archivos de tu propia lista o lo que sea siempre usas funciones diferentes para cada petición... Sólo si tienes una página donde cargas siempre el mismo XML/JSON o lo que sea necesitas una sola función. En ese caso, modifica un poco la función Ajax y sustituye b por tu función especial.

Las funciones anteriores son para uso básico. Si quieres EXTENDER la función... Sí, puedes hacerlo. Yo uso muchas APIs y una de las primeras funciones que integro en cada página HTML es la primera función Ajax de esta respuesta, sólo con GET... Pero puedes hacer muchas cosas con XMLHttpRequest 2: Hice un gestor de descargas (usando rangos en ambos lados con resume, filereader, filesystem), varios conversores de imágenes usando canvas, poblar bases de datos web SQL con base64images y mucho más... Pero en estos casos deberías crear una función sólo para ese propósito... a veces necesitas un blob, array buffers, puedes establecer cabeceras, anular mimetype y hay mucho más... Pero la cuestión aquí es cómo devolver una respuesta Ajax... (He añadido una forma fácil).

Comentarios (7)