De toekomst van Javascript deel 1: Promises

De ontwikkeling van Javascript is de laatste jaren in zo’n stroomversnelling gekomen dat we spreken van Browser Wars 2.0 en voorlopig komt daar nog geen einde aan. De ontwikkeling van Node.js en het hele ecosysteem er omheen gaat gestaag verder en de ratificatie van de nieuwe versie van EcmaScript staat gepland voor december dit jaar.

In deze serie artikelen bekijk ik de nieuwe ontwikkelingen in Javascript met in dit eerste deel: Promises.

 

Wat is een Promise?

pagerEen Promise is een object dat een waarde in de toekomst weergeeft. Als je kinderen hebt ben je vast wel eens in een indoor speeltuin geweest en heb je iets te eten besteld aan de balie. Als je hebt besteld krijg je een soort van pager mee die begint te knipperen en te trillen als je eten klaar staat. Dat is een Promise.

Een Promise is het object wat je terug krijgt uit een function call wanneer de waarde die de functie moet teruggeven niet direct beschikbaar is, maar je er niet op wilt wachten en verder wilt met het script (asynchroon dus). Je krijgt dan een Promise object terug waar je een eventhandler aan kunt hangen welke uitgevoerd wordt als de functie klaar is of je kunt dit object weer aan een andere functie geven.

Een bekend voorbeeld van een functie die een Promise genereert is een jQuery ajaxcall. Deze functie geeft tegenwoordig standaard een object terug wat de Promise interface implementeert. Traditioneel wordt de functie die moet worden uitgevoerd geïmplementeerd als een callback, maar bij een Promise wordt de callback als argument gegeven aan een method als then of done:

$.get("test.html")
.done(function() {
    // callback code
});

Met de method then of een combinatie van then en done kunnen meerdere callbacks na elkaar worden uitgevoerd die steeds op hun beurt weer een Promise teruggeven:

$.get("test.html")
.then(function() {
    // callback code
})
.then(function() {
    // callback code
})
.done(function() {
    // callback code
});

Maar wat is er mis met callbacks?

In principe is er natuurlijk niets mis met een callback, maar het wordt lastiger wanneer je te maken krijgt met geneste callbacks. Wanneer bovenstaand voorbeeld geïmplementeerd zou worden met callbacks zouden deze genest moeten worden om ervoor te zorgen dat de callbacks in de juiste volgorde uitgevoerd worden:

$.get("url1.html", function() {
    $.get("url2.html", function() {
        $.get("url3.html", function() {
            // callback code
        });
    ​});
});

Niet bepaald de meest aantrekkelijke code die ook nog eens steeds onleesbaarder wordt naarmate er meer callbacks zijn en de code steeds meer naar links gaat. Maar dat is eigenlijk nog het kleinste probleem en ook wel enigszins op te lossen door de callbacks in een variabele te stoppen:

$.get("url1.html", callback1());

function callback1() {
    $.get("url2.html", callback2());
});

function callback2() {
    $.get("url3.html", callback3());
});

function callback3() {
    // callback code
});

Het echte probleem hier is echter dat de callback hard-coded verwijzingen naar elkaar bevatten en dus niet meer zonder aanpassingen herbruikbaar zijn. De functie callback1 bevat een hard-coded verwijzing naar callback2 en wanneer $.get een andere callback moet krijgen moet callback1 ook aangepast worden. Opnieuw geen onoverkomelijk probleem, maar niet bepaald ideaal en de code wordt er ook niet duidelijker van. Bovendien wordt het ook erg lastig wanneer third-party code als callback wordt gebruikt waarin je geen callback kunt nesten. Daar zou je dan een wrapper functie omheen kunnen bouwen maar als het drie van die callbacks zijn worden het al drie wrapper functies. Pfff…

Met Promises is dit dus veel eenvoudiger op te lossen omdat je de uit te voeren callbacks gewoon in een soort pijplijn achter elkaar zet, simpel en overzichtelijk. Promises brengen functional composition terug in asynchroon programmeren en dat is veel belangrijker dan voorkomen dat je code minder goed leesbaar wordt door die geneste callbacks.

Synchroon vs asynchroon

In “gewoon” synchroon programmeren is compositie van functies eenvoudig. Je kunt de waarde die de ene functie teruggeeft weer als argument aan een andere functie geven en de waarde die dan wordt teruggegeven weer als argument aan een functie geven etc. Als er ergens een exception optreedt kan die worden opgevangen door een catch en stopt de hele keten:

try {
    var a = funcA();
    var b = funcB();
    var d = funcD(funcC(b));
}
catch(e) {
    console.log('Oops!', e.message);
}

Met asynchroon programmeren is dit anders. Een asynchrone functie geeft niet direct een waarde terug en waar moeten we dan precies die try/catch plaatsen? In elke geneste callback? Promises lossen deze twee problemen op en geven ons dus twee dingen:

  • compositie van asynchrone functies
  • exception handling voor (een compositie van) asynchrone functies

Hoe en wanneer kan ik Promises gebruiken?

Native Promises worden bij het schrijven van dit artikel alleen ondersteund door Chrome 33 en Firefox Nightly (Firefox 29) maar er zijn libraries als Q die Promises volgens de specificaties implementeren en die je vandaag kunt gaan gebruiken.

Laten we eens kijken hoe een native Promise werkt:

function asyncFunc(){
    var promise = new Promise(function(resolve, reject) {
    var success = true;  // in productie wordt de waarde van deze variabele bepaald door het resultaat van een asynchrone call

    if(success) {
        resolve('OK!');
    } 
    else {
        reject('Oops!');
    }

    return promise;
});

In de bovenstaande functie asyncFunc wordt een Promise gegenereerd en teruggegeven. Als in deze functie nu een asynchrone call wordt gedaan en er dus niet direct iets teruggegeven wordt dan stelt de teruggegeven Promise de terug te geven waarde voor. De Promise krijgt als argument een callback welke twee argumenten krijgt: een callback voor een succesvolle call en een callback voor wanneer er een error optreedt. In het voorbeeld is dit alleen maar afhankelijk van de variabele success maar in echte productiecode zou dit natuurlijk afhangen van het al dan niet succesvol uitvoeren van de asynchrone call. 

Omdat asyncFunc nu een Promise teruggeeft kunnen we de callbacks voor succes en error hier direct aanhangen met then:

asyncFunc().then(function(result) {
    console.log('Call gelukt met resultaat: '+result);
}, function(err) {
    console.error('Er is een fout opgetreden: '+err);
});

Een heel simpel voorbeeld maar het maakt duidelijk hoe Promises werken. In het bovenstaande voorbeeld zal de eerste callback uitgevoerd worden en wanneer de variabele success de waarde false krijgt wordt de tweede callback uitgevoerd. Bovendien wordt er een variabele result aan de callback gegeven. De Promise is dan resolved of rejected met deze waarde. Wanneer de callback ook weer een Promise teruggeeft kunnen we daar opnieuw een callback aan hangen met then en zo kunnen we door gaan:

asyncFunc()
.then(callback1)
.then(callback2)
.then(callback3);

Op die manier maken Promises dus compositie van asynchrone functies mogelijk. Maar hoe zit het dan met errorhandling voor asynchrone functies?

Blij dat je het vraagt.

De method then van een Promise krijgt steeds twee argumenten: een callback voor succes en een callback welke wordt uitgevoerd wanneer er een error optreedt. Op die manier kunnen we in elke stap, dus iedere keer wanneer er een functie wordt uitgevoerd die een Promise teruggeeft (asyncFunc of één van de callbacks) een error afhandelen met die error-callback. Maar soms willen we alleen errors die ergens in het proces optreden afhandelen met één en dezelfde errorhandler. En dat kan. 

Het is je wellicht opgevallen dat then in het bovenstaande voorbeeld steeds maar één callback krijgt (die voor succes) en geen error-callback. Als we eventuele errors niet per stap willen afhandelen is een error-callback per stap niet nodig en doe je dat met de method catch:

asyncFunc()
.then(callback1)
.then(callback2)
.then(callback3)
.catch(function() {
    console.error('Oops!);
});

Wanneer er nu ergens in het proces een error optreedt wordt de callback die als argument aan catch wordt gegeven uitgevoerd en verschijnt de uiterst informatieve foutmelding “Oops!” in de console.

Dit is in feite hetzelfde als deze synchrone code:

try {
    asyncFunc() 
    callback1();
    callback2();
    callback3();   
}
catch(e) {
    console.error('Oops!);
}

Ik zeg “in feite” omdat de vergelijking eigenlijk alleen opgaat voor de manier van errorhandling. In dit voorbeeld hebben de callbacks bijvoorbeeld toegang tot dezelfde scope, wat met callbacks van Promises niet het geval is tenzij er variabelen worden doorgegeven. Een echte asynchrone variant van deze code kan alleen bereikt worden met Generators, waarover je meer kunt lezen in deel 2 van deze serie. Waar het hier nu echter om gaat is dat errors die waar dan ook optreden in de keten van Promises afgehandeld kunnen worden met één errorhandler, analoog aan een try/catch constructie voor synchrone code.

De method catch is syntactic sugar voor:

.then(undefined, errorCallback);

Waarden doorgeven met Promises

Wanneer je Promises met then aan elkaar koppelt dient elke handler die als (eerste) argument aan then wordt gegeven ook een Promise terug te geven. Je kunt ook waarden doorgeven wanneer de handler niet persé een Promise terug hoeft te geven, bijvoorbeeld omdat er geen asynchrone call gedaan wordt:

var promise = new Promise(function(resolve, reject) { 
    resolve(1); 
}); 

promise.then(function(val) { 
    console.log(val); // 1 
    return val + 2; 
})
.then(function(val) { 
    console.log(val); // 3 
});

Wanneer je dus een waarde teruggeeft wordt de volgende then meteen aangeroepen met die teruggegeven waarde als argument. Dit in tegenstelling tot wanneer je een Promise teruggeeft, dan zal then pas aangeroepen worden wanneer de Promise resolved of rejected is. 

Cool! Maar ik wil Promises NU gebruiken!

No problemo! Zoals ik al eerder schreef zijn er diverse libraries die Promises volgens de specificaties implementeren zoals:

Verder biedt jQuery het Deferred object wat niet geheel volgens de specificaties is. Dit is op zich niet zo heel erg maar op het gebied van error handling is de implementatie zodanig anders dat ook ik vind dat je rustig van een foute implementatie kunt spreken. Ik ga nier niet teveel in op de details hiervan, maar als je jQuery gebruikt raad ik je met klem aan om dit artikel te lezen. De AJAX methods van jQuery zoals o.a. $.ajax en $.get implementeren vanaf versie 1.5 ook de Promise interface dus je kunt dan met één van deze methods een Promise genereren en teruggeven:

var foo = function() {
    var promise = $.ajax('http://example.com');
    ....
    return promise;
}

De andere libraries zoals Q bieden een uitgebreide API die vaak verder gaat dan de API van native Promises. Voor de details hiervan verwijs ik je naar de documentatie van deze libraries en voor de documentatie van de native Promises verwijs ik je graag naar Mozilla Developer Network.

Conclusie

Promises zijn een krachtige aanvulling op Javascript die het schrijven van asynchrone code makkelijker en overzichtelijker maakt, maar vooral nuttig is omdat het compositie en errorhandling van asynchrone functies mogelijk maakt. Dat laatste is wat Promises pas écht interessant maakt. Mobiel internet wordt steeds belangrijker, websites steeds complexer en daar komt steeds meer asynchroon programmeren bij kijken waarvoor Promises onmisbaar zijn (tenzij je erg masochistisch ingesteld bent en graag callback spaghetti schrijft).

Maar Promises zijn pas het begin, net zoals dit artikel alleen nog maar een introductie in Promises is.

In deel 2 van deze serie de volgende stap om asynchroon programmeren nog makkelijker en beter te maken: Generators.

Verder lezen?

De toekomst van Javascript deel 1: Promises by

Over de auteur

pasfotoDanny Moerkerke is oprichter van Progblog.nl en freelance full-stack webdeveloper.

Naast progblog.nl publiceert hij ook op zijn eigen blog dannymoerkerke.nl.

Hij hoopt dat het ooit nog eens mogelijk wordt om kennis rechtstreeks in zijn hoofd te kunnen pluggen zoals in The Matrix.

Twitter: @dannymoerkerke
LinkedIn
Google+