Oswojony Zalgo
Wieki temu Isaac Z. Schlueter napisał artykuł na temat projektowania asynchronicznyczh API. Przestrzegł w nim przed wypuszczaniem na świat Zalgo.
Zalgo?
Zalgo czyha poza kurtyną poznania, by swymi długimi mackami siać zamęt i terror w umysłach ludzi niezdolnych do pojęcia jego wszechpotężnej grozy. I nadchodzi, jest coraz bliżej…
A w kontekście artykułu chodzi po prostu o API, które czasami zachowuje się asynchronicznie, a czasami – synchronicznie. Przez to trudno jest w pełni przewidzieć jego zachowanie, a tym samym – zarządzać przepływem w aplikacji. Żeby zatem nie wypuszczać Zalgo w swoim kodzie i nie powodować szaleństwa wśród innych programistów, wypada tworzyć API, które zawsze są synchroniczne albo zawsze asynchroniczne.
Zalgo!
Przypomniałem sobie o asynchronicznym Zalgo przy pisaniu nowego projektu. Moje publiczne API zwykle są asynchroniczne, więc, niewiele myśląc, naskrobałem coś takiego:
async function publicAPI( data ) {
return data.repeat( 2 );
}
Teraz tylko jeszcze dodać jeszcze walidację przekazanych danych:
async function publicAPI( data ) {
if ( typeof data !== 'string' ) {
throw new TypeError( 'I need a string!!!' );
}
return data.repeat( 2 );
}
Voilà! Tylko że nie zachowywało się to tak, jak publiczne API w moich innych bibliotekach… Porównałem zatem z jakimś innym moim kodem:
function publicAPI( data ) {
if ( typeof data !== 'string' ) {
throw new TypeError( 'I need a string!!!' );
}
return new Promise( ( resolve ) => {
resolve( data.repeat( 2 ) );
} );
}
Co Bardziej Rozgarnięty Czytelnik w tym momencie pewnie już wie, gdzie czai się mroczn leży problem. Większość moich API wypuszczała Zalgo. W chwili, gdy występował w nich błąd, błędy były rzucane synchronicznie, podczas gdy wynik był zwracany asynchronicznie. Funkcja asynchroniczna z kolei zawsze rzuca błędy i zwraca wynik asynchronicznie:
function zalgoAPI( data ) {
if ( typeof data !== 'string' ) {
throw new TypeError( 'I need a string!!!' );
}
return new Promise( ( resolve ) => {
resolve( data.repeat( 2 ) );
} );
}
async function safeAPI( data ) {
if ( typeof data !== 'string' ) {
throw new TypeError( 'I need a string!!!' );
}
return data.repeat( 2 );
}
try {
zalgoAPI( 1 );
} catch( error ) {
console.error( error );
}
safeAPI( 1 ).catch( console.error );
Problem robi się dość oczywisty, gdy chcemy dorobić obsługę błędów dla takiego zalgoconego API. Bo musimy dorobić ją podwójnie: jedną, synchroniczną, dla walidacji danych i drugą, asynchroniczną, dla błędów, jakie mogą pojawić się w czasie rozwiązywania wynikowej obiecanki:
try {
zalgoAPI( someValue ).catch( console.error );
} catch( error ) {
console.error( error );
}
W moim kodzie najczęściej problem omijałem, stosując składnię async
/await
, która wszystkie błędy pozwala łapać przy pomocy try
/catch
:
function zalgoAPI( data ) {
if ( typeof data !== 'string' ) {
throw new TypeError( 'I need a string!!!' );
}
return new Promise( ( resolve, reject ) => {
reject( new Error( 'A kuku!' ) );
} );
}
( async function() {
try {
await zalgoAPI( 1 );
} catch( error ) {
console.error( error ); // Błąd walidacji.
}
try {
await zalgoAPI( 'string' );
} catch( error ) {
console.error( error ); // Błąd z obiecanki.
}
}() );
Nie zmienia to faktu, że powinno to działać także poza składnią async
/await
, przy wykorzystaniu samych Promise
. Czeka mnie zatem nieco refaktoryzacji…
Chociaż dziwne, że tak długo wydawało mi się wręcz, że mój sposób jest bardziej intuicyjny. Czyżbym zaraził się szaleństwem Zalgo?