10. Promesas de resultados
Una promesa, o deferred object, es una herramienta muy sencilla y útil cuando realizamos programación asíncrona.
Aunque hay muchas implementaciones en JavaScript, el equipo de AngularJS realizó una adaptación de la librería Q, de Kris Kowal, debido a su éxito y difusión.
10.1. Por qué utilizar promesas
En JavaScript, los métodos asíncronos normalmente utilizan funciones de callback para devolver un estado de éxito o error. Por ejemplo, la API de gelocalización requiere de estas dos funciones de callback para obtener la posición actual:
1
2
3
4
5
6
7
8
9 var successFn = function(response){
console.log('SUCCESS! ' + JSON.stringify(response));
};
var errorFn = function(err) {
console.log('ERROR! ' + JSON.stringify(response));
}
navigator.geolocation.getCurrentPosition(successFn, errorFn);
Otro ejemplo es el objeto XMLHttpRequest, que utilizamos para realizar peticiones AJAX. Tiene una función de callback llamada onReadyStateChange
, que se llama cuando cambia el atributo readyState
.
1
2
3
4
5
6
7
8
9
10 var xhr = new window.XMLHttpRequest();
xhr.open('GET', 'http://www.webdeveasy.com', true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
console.log('Success');
}
}
};
xhr.send();
En nuestro día a día nos encontraremos con una infinidad de usos y ejemplos. Aunque puede parecer sencillo de manejar, puede volverse un infierno cuando tenemos que encadenar funciones de sincronización.

La imagen ilustra un ejemplo de la famosa callback pyramid of doom. Aunque hay maneras más elegantes de escribir y refactorizar el código, siempre será difícil de leer y mantener.
10.2. Promesas y deferred objects
Una promesa representa el resultado de una operación asíncrona. Expone una interfaz que puede usarse para interactuar con el resultado que tendrá dicha operación. Así, también permite que quien esté interesado pueda hacer uso de dicho resultado.
La promesa está asociada a un deferred object, cuyo estado será "pendiente", y no tiene ningún resultado. Cuando invoquemos los métodos resolve()
o reject()
, este estado pasará a "resuelto" o "rechazado". Además, podemos coger la promesa una vez inicializada y definir operaciones con su resultado futuro, que se llevarán a cabo cuando se cambie a los estados "resuelto" o "rechazado" que acabamos de mencionar.
Mientras el deferred object tiene métodos para cambiar el estado de una operación, la promesa sólo expone métodos para operar con el resultado. Es por ello que es una buena práctica devolver una promesa y no un deferred object.
10.3. Promesas en AngularJS
En primer lugar, deberemos crear un deferred object:
1 var deferred = $q.defer();
El objeto deferred
apunta a un deferred object, cuyo estado podremos resolver o rechazar tras realizar una operación asíncrona. Supongamos el método asíncrono async(successFn, errorFn)
, donde los dos parámetros son funciones de callback. Cuando async
finaliza su ejecución, queremos resolver o rechazar deferred
con su resultado:
1
2
3
4
5
6
7
8 async(
function(val){
deferred.resolve(val);
},
function(err){
deferred.reject(err);
}
);
Incluso podemos simplificar la llamada, ya que los métodos resolve
y reject
no precisan de un contexto:
1 async(deferred.resolve(val), deferred.reject(err));
Ahora, asignar operaciones una vez haya habido éxito o error es bastante sencillo:
1
2
3
4
5
6
7 var promise = deferred.promise;
promise
.then(
function(data){ alert('Success! ' + data); },
function(data){ alert('Error! ' + data); }
)
Podemos asignar tantas funciones de éxito o error como queramos. En el siguiente ejemplo, tanto las funciones asignadas antes de la llamada a async
como las que se realizan después se ejecutarán al resultado:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 var deferred = $q.defer();
deferred.promise
.then(function(data) {
console.log('Success asignado antes de invocar async()', data);
}, function(error) {
console.log('Error asignado antes de invocar async()', error);
});
async(deferred.resolve, deferred.reject);
deferred.promise
.then(function(data) {
console.log('Success asignado tras invocar async()', data);
}, function(error) {
console.log('Error asignado tras invocar async()', error);
});
Hemos visto que el método then
recibe dos funciones, una de éxito y una de error. Sin embargo, podemos usar then
para asignar funciones de éxito, y utilizar catch
cuando se produce un error. Además, existe una función finally
, que se ejecutará siempre, se haya resuelto correctamente o no. Gracias a finally
, no tendremos que duplicar código que se podría ejecutar tanto en la parte del éxito como la del error.
1
2
3
4
5 deferred.then(function(){ console.log('All OK')});
deferred.catch(function(){ console.log('Error!')});
deferred.finally(function(){ console.log('End.'); });
10.4. Encadenando promesas
Un dato interesante que hay que conocer, es que el método then
de una promesa devuelve otra promesa. Cuando resolvemos la primera promesa, el valor que devolvamos se enviará a la promesa siguiente, de manera que podemos encadenar y transformar una serie de promesas de resultados. Veámoslo con un ejemplo concreto:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 function async(value) { // Supongamos que se trata de una operación asíncrona de verdad
var deferred = $q.defer();
var asyncCalculation = value / 2;
deferred.resolve(asyncCalculation);
return deferred.promise;
}
var promise = async(8)
.then(function(x) {
return x+1;
})
.then(function(x) {
return x*2;
})
.then(function(x) {
return x-1;
});
promise.then(function(x) {
console.log(x);
});
La promesa empieza con llamando a async(8)
, que resuelve con el valor 4
. Este valor pasa por todas las funciones then
secuencialmente, hasta pintar el valor 9
, ya que hace (8 / 2 + 1) * 2 - 1
.
Como hemos visto antes que las funciones no necesitan contexto, podemos refactorizarlo de la siguiente manera:
1
2
3
4
5 var promise = async(8)
.then(addOne)
.then(mult2)
.then(minusOne)
.then(paintValue);
El ejemplo es muy optimista y asume que todo va a ir bien. Pero si no es así, ¿dónde colocamos nuestro catch
? Bien, en caso de operaciones encadenadas, catch
y finally
se colocan en último lugar:
1
2
3
4
5
6
7 var promise = async(8)
.then(addOne)
.then(mult2)
.then(minusOne)
.then(paintValue)
.catch(showError)
.finally(endFn);
En el momento que cualquier elemento resuelva incorrectamente (ya sea el primero o el tercero), se ejecutará a continuación el catch
, y se terminará con el finally
.
10.5. Otros métodos útiles
10.5.1. $q.reject
En algunas ocasiones, puede que necesitemos devolver una promesa rechazada. En lugar de crear una promesa y rechazarla, podemos usar $q.reject(reason)
, que devuelve una promesa rechazada, con el motivo que le digamos. Por ejemplo:
1
2
3
4
5
6
7
8
9
10 var promise = async()
.then(
function(value){
if(isValid(value)) {
return value;
}
return $q.reject('Valor no válido');
}
);
Si value
es válido, un conjunto de promesas encadenadas funcionará correctamente. Sin embargo, se irá al bloque catch
si no es válido.
10.5.2. $q.when
Similar a $q.reject
, pero devuelve un valor correctamente resuelto. Un ejemplo muy claro de uso es cuando tenemos que pedir un dato al servidor si no lo tenemos cacheado.
1
2
3
4
5
6
7 function getElement(key){
if(!!$localStorage.key) {
return $q.when($localStorage.key);
} else {
return getFromServer(key);
}
}
10.5.3. $q.all
En algunas ocasiones podríamos querer tener una serie de elementos de manera asíncrona, sin importarnos el orden, y ser notificados al terminar. Para ello, hacemos uso de $q.all(promisesArray)
. Devuelve una promesa que se resuelve sólo cuando todas las promesas del array se han resuelto. Si al menos una de las promesas del array se rechaza, también lo hará el resultado de $q.all
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 var allPromises = $q.all([
async1(),
async2(),
async3(),
...
asyncN()
]);
allPromises.then(function(values){
var value1 = values[0];
var value2 = values[1];
var value3 = values[2];
...
var valueN = values[N+1];
console.log('end');
});
10.6. Ejercicio (0.5 puntos)
Genera un servicio en AngularJS que haga uso de promesas y de la API de geoposicionamiento para devolver las coordenadas del navegador. Haz también un programa en AngularJS que pinte las coordenadas en pantalla.
Aplica el tag promises
a la versión que quieres que se corrija.