Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

La gestione degli errori in un'app Windows Store

Link copiato negli appunti

In un'applicazione Windows Store, come del resto in una qualunque altra applicazione .NET, la gestione degli errori che possono verificarsi durante l'esecuzione rappresenta un momento particolarmente delicato.

Gestire gli errori in modo che non raggiungano l'utente

In un'app sviluppata in C# o VB, il Common Language Runtime (CLR) fornisce un modello uniforme per la notifica degli errori, in base al quale qualunque operazione è tenuta a notificare eventuali errori attraverso il sollevamento di eccezioni. Queste eccezioni si propagano lungo la catena di chiamate, fino a quando non vengono intercettate in un blocco try/catch/finally ovvero finiscono per determinare la chiusura dell'applicazione.

Molti di questi concetti, e relative best practice, valgono anche per le applicazioni Windows Store sviluppate in JavaScript, per quanto la particolare natura del linguaggio presenti diverse peculiarità, se confrontato con il modello offerto dal CLR, in particolare quando si ha a che fare con promise .

Ad esempio, è fondamentale circondare il proprio codice in blocchi try/catch/finally ogni volta che vi sia la possibilità che qualcosa vada storto, soprattutto quando si tenta di accedere a una risorsa che potrebbe essere non disponibile o mancante, o quando si utilizzano tipi e funzioni sulle quali non si ha alcun controllo (come nel caso di librerie di terze parti, servizi remoti, ecc.).

Il seguente snippet mostra un esempio base di gestione degli errori in un'applicazione Windows Store in HTML5/JavaScript.

app.onloaded = function () {
    mainMethod();
}
function mainMethod() {
    try {
        doSomeWork(10);
    } catch (e) {
        Debug.writeln(e.message);
    }
}
function doSomeWork(name) {
    var result = name.toUpperCase();
    return result;
}

La funzione DoSomeWork solleva un'eccezione, sotto forma di un oggetto Error, quando tenta di convertire il numero intero passato come parametro in una stringa di testo. L'eccezione è intercettata e gestita dal blocco try/catch che circonda la chiamata al metodo. È anche possibile verificare il tipo di eccezione sollevata a runtime tramite l'istruzione instanceOf, in modo da gestire diversi tipi di eccezione, come mostrato nel prossimo snippet.

function mainMethod() {
    try {
        sampleText.innerHTML = "
" + doSomeWork(0) + "
";
    } catch (e) {
        if (e instanceof TypeError) {
            // gestione specifica di questo particolare tipo di errore
        }
        else {
            // gestione di default degli errori
        }
    }
}

I tipi di errore in JavaScript, tutti derivati dal tipo base Error, sono i seguenti:

Errore Descrizione
EvalError viene sollevato quando si verifica un errore durante l'esecuzione della funzione eval
TypeError viene sollevato quando il tipo dell'operatore non coincide con quello previsto
ConversionError si verifica quando il tentativo di convertire un oggetto in un differente tipo fallisce
RangeError si verifica quando si tenta di assegnare a una variabile un valore che va oltre il limite massimo consentito (ad esempio, un numero che eccede il valore Number.MAX_VALUE)
ReferenceError sollevato quando si tenta di accedere a una variabile che non esiste
URIError si verifica quando si tenta di utilizzare una URI malformata, generalmente a causa di un erorre nella codifica o decodifica
SyntaxErorr il nome è piuttosto auto-esplicativo; questo errore accade soprattutto quando si tenta di passare al metodo eval una stringa con un errore di sintassi

Nelle proprie funzioni, è sempre meglio sollevare un'eccezione, piuttosto che restituire codici di errore, valori booleani, messaggi testuali o valori null. I valori restituiti, infatti, non propagandosi attraverso la catena di chiamate, potrebbero andare persi e quindi lasciare l'app in uno stato inconsistente o determinare comportamenti imprevedibili (restituire null può invece essere un'opzione valida nel caso di "errori" particolarmente comuni, come ad esempio un record non trovato). Il codice che segue solleva un'eccezione quando il parametro passato non è una stringa:

function doSomeWork(name) {
	if (typeof name != "string") {
		throw new TypeError("Invalid parameter.");
	}
	var result = name.toUpperCase();
	return result;
}

Quando viene sollevata un'eccezione, è importante ricordarsi di eliminare qualunque risultato intermedio: il chiamante deve poter contare sul fatto che, se qualcosa va storto durante l'esecuzione di un metodo, lo stato dell'oggetto coinvolto non è stato alterato.

È anche possibile creare tipi di errore custom, magari derivandoli dalla classe Error. Il prossimo listato mostra un esempio di questo scenario, in cui un errore custom viene sollevato dal metodo doSomeWork per segnalare che l'oggetto ricevuto come parametro è del tipo sbagliato.

function mainMethod() {
    try {
        doSomeWork(0);
    } catch (e) {
        if (e instanceof MyCustomError) {
            // gestione specifica di questo particolare tipo di errore
        }
        else {
            // gestione di default degli errori
        }
    }
}
function doSomeWork(name) {
    if (typeof name != "string") {
        throw new MyCustomError("001", "Invalid parameter");
    }
    var result = name.toUpperCase();
    return result;
}
function MyCustomError(code, message) {
    this.code = code;
    this.message = message;
}

La Windows Library for JavaScript (WinJS) mette a disposizione un modo per intercettare eccezioni "sfuggite" a qualunque blocco catch, e dunque non gestite ("unhandled") dal codice applicativo (esistono in realtà due strade per raggiungere questo risultato, ma la seconda è specifica per le promise; avremo modo di tornare su questo punto più avanti).

In particolare, l'oggetto WinJS.Application, che incapsula funzioni come l'attivazione dell'applicazione, lo storage e i vari eventi applicativi, espone uno special evento denonominato error, che viene sollevato ogniqualvolta un'eccezione non gestita raggiunge il livello applicativo. Prima di vedere come sfruttare questo evento, è interessante osservare cosa succede "under the hood" quando un'eccezione viene sollevata durante il debugging dell'applicazione.

Il prossimo snippet mostra come il file base.js, incluso in WinJS, gestisce questo evento.

(...)
error: [
    function Application_errorHandler(e, handled) {
        if (handled) {
            return;
        }
            (...)
            WinJS.Application._terminateApp(terminateData, e);
        }
    }
],
(...)
var terminateAppHandler = function (data, e) {
    (...)
    debugger;
    MSApp.terminateApp(data);
};

Nell'event handler, se il valore booleano passato come secondo parametro (handled) è true, significa che l'errore è stato gestito dal codice, in caso contrario il debugger viene messo in pausa e l'applicazione terminata.

Se vogliamo gestire questo evento dal nostro codice, è bene abbonarsi all'evento error quanto prima possibile nel ciclo di vita dell'app. Il prossimo snippet ne mostra un esempio:

var app = WinJS.Application;
app.onerror = errorListener;
function errorListener(err) {
    new Windows.UI.Popups.MessageDialog(err.detail.errCode).showAsync();
    return true;
}

Quando viene sollevata un eccezione non gestita, l'event handler mostra una dialog all'utente informandolo del problema. Quindi restituisce true per segnalare che l'eccezione è stata gestita e che l'app non necessita di essere terminata. Se omettessimo il valore di ritorno, o se esplicitamente restituissimo false, l'app verrebbe immediatamente chiusa.

È importante precisare, però, che evitare la chiusura dell'app in presenza di un'eccezione non gestita non sempre è una buona idea. Infatti, dato che non c'è modo di sapere da dove arrivi l'eccezione (altrimenti l'avremmo gestita), alcune parti dell'applicazione potrebbero trovarsi in uno stato inconsistente a seguito dell'errore. Piuttosto, lo scopo dell'eventoè quello di consentire, in presenza di un'eccezione non prevista, di "prepararsi" alla chiusura dell'applicazione, ad esempio per loggare l'eccezione (dal momento che è sfuggita a qualunque blocco try/catch, l'handler dell'evento error è l'ultimo posto in cui possiamo sperare di loggare l'eccezione), o magari salvare dati temporanei nello storage.

Per completezza, ricordiamo che, in alternative all'evento WinJS.Application.error, è anche possibile utilizzare il classico evento JavaScript window.onerror per intercettare eccezioni non gestite da codice: i due eventi funzionano in modo simile.

Finora abbiamo avuto a che fare con eccezioni sincrone, ossia eccezioni sollevate durante l'esecuzione di codice asincrono. Tuttavia, molte delle API di WinRT sono esposte tramite funzioni asincrone, vale a dire mediante promise. Gestire le eccezioni sollevate durante l'esecuzione di operazioni asincrone, soprattutto se concatenate una dietro all'altra, può rapidamente diventare un'impresa impegnativa. In questo tipo di scenari, è necessario contare su meccanismi che non siano i classici blocchi try/catch (come invece avviene nelle applicazioni Windows Store in C#/VB).

Errori e promise

Come abbiamo già avuto modo di vedere nell'articolo di questa guida dedicato ai pattern asincroni in JavaScript, una promise può essere definita come la "promessa" di restituire un certo valore in un qualche momento futuro. Non è possibile sapere esattamente quando questo valore sarà disponibile, ma possiamo schedulare l'esecuzione di un blocco di codice nel momento in cui la promise sarà stata completata ("fulfilled"). A differenza delle tradizionali callback, una promise espone un'interfaccia standard per interagire con il codice asincrono. Ogni promise espone infatti dei metodi standard, come then e done, mentre nel caso di callback, è la firma del metodo a definire quali sono i parametri da passare, e in quale ordine.

Abbiamo anche visto come esista un'importante differenza tra then e done in relazione alla gestione delle eccezioni. Dal momento che la funzione then restituisce a sua volta una promise, l'eccezione viene catturata come parte dello stato della promise, che potrà essere successivamente ispezionato tramite successive chiamate ai metodi then o done. Questo significa che le eccezioni sollevate durante l'esecuzione dell'operazione asincrona non raggiungeranno mai la superficie, e non ci sarà modo di sapere se qualcosa è andato storto durante l'operazione.

Vediamo un esempio per chiarire meglio il concetto:

function getFile(folderName, fileName) {
    Windows.ApplicationModel.Package.current.installedLocation.getFolderAsync(folderName)
        .then(function (folder) {
            return folder.getFileAsync(fileName);
        })
        .then(function (file) {
            // operazione su file
        });
}

In questo scenario, se si verifica un errore in una delle due promise lungo la catena, il valore restituito sarà a sua volta una promise, e l'eccezione verrà salvata nello stato della promise restituita. Lo stato di una promise può infatti assumere uno dei seguenti valori (rappresentati dall'enum Windows.Foundation.AsyncStatus):

Valore Descrizione
Canceled l'operazione è stata cancellata
Completed l'operazione è stata completata
Error si è verificato un errore durante l'operazione
Started l'operazione è stata avviata

Quando viene restituita una promise con lo stato di errore, il codice controlla se è stata fornita una funzione di errore (nota anche come funzione di "fallback") nel successivo then nella catena. Se la funzione non è presente, verrà creata una nuova promise, a sua volta con il suo stato di errore, e verrà passata al successivo anello della catena.

Se, risalendo la catena, nessuna chiamata al metodo then prevede una funzione di fallback, l'eccezione verrà semplicemente "ingoiata" e andrà persa. In questo modo, l'utente non potrà sapere che qualcosa non ha funzionato correttamente, né a che punto della catena si è verificato l'errore, con il rischio di lasciare l'applicazione in uno stato inconsistente.

Un modo di ovviare al problema è quello di passare una funzione di fallback (almeno) all'ultimo then della catena, in modo da poter gestire un'eventuale eccezione sollevata durante l'esecuzione di una delle operazioni asincrone. Il prossimo snippet ne mostra un esempio:

function getFile(folderName, fileName) {
    Windows.ApplicationModel.Package.current.installedLocation        .getFolderAsync(folderName)
    .then(function (folder) {
        return folder.getFileAsync(fileName);
    })
    .then(function (file) {
        // esegue una qualunque operazione sul file.
        // se viene sollevata un’eccezione in questo punto, l’errore andrà perso
    },
    function (err) {
        // questa funzione viene chiamata quando viene sollevato
 // un errore durante l’esecuzione delle precedenti operazioni asincrone
    });
}

Il problema in questo codice è che, se un errore si verifica durante l'esecuzione della funzione di callback che chiude la catena, l'eccezione andrà comunque persa. Per questa ragione, è sempre meglio chiudere la catena di promise con una chiamata alla funzione done, invece che then, come mostrato nel seguente snippet:

function getFile(folderName, fileName) {
    Windows.ApplicationModel.Package.current.installedLocation.getFolderAsync(folderName)
    .then(function (folder) {
        return folder.getFileAsync(fileName);
    })
    .done(function (file) {
        // se si verifica un errore in un punto qualunque della catena, viene sollevata un'eccezione
    });
}

Con un done al termine della catena di promise, se si verifica un errore durante una qualunque delle operazioni asincrone e nessuna funzione di fallback è stata esplicitamente passata come parametro, il metodo done solleva un'eccezione (a differenza di then, la funzione done non restituisce a sua volta una promise). Questa eccezione si propagherà nell'Event Loop, che potrà essere eventualmente intercettata mediante a uno specifico evento messo a disposizione da WinJS (che vedremo fra breve), e non tramite un eventuale blocco try/catch posto attorno alla catena di chiamate asincrone. Oppure possiamo fornire una funzione di fallback al metodo done, e in questo caso sarà questo il luogo dove gestire l'errore.

Il codice che segue mostra un esempio di quest'ultimo scenario:

function getFile(folderName, fileName) {
    Windows.ApplicationModel.Package.current.installedLocation.getFolderAsync(folderName)
    .then(function (folder) {
        return folder.getFileAsync(fileName);
    })
    .done(function (file) {
        // se si verifica un errore in un punto qualunque della catena, viene sollevata un'eccezione
    }, function (err) {
        // questa funzione viene chiamata quando viene sollevato
        // un errore durante l’esecuzione delle precedenti operazioni asincrone
    });
}

Se qualcosa va storto durante l'esecuzione del metodo done che chiude la catena di promise (a prescindere dal fatto che l'errore si verifichi nella funzione di callback, di fallback o di progresso), verrà sempre sollevata un'eccezione (mentre nel caso della funzione then l'eccezione sarebbe stata "ingoiata").

Anziché concatenare chiamate alle funzioni then/done al termine di ciascuna operazione asincrona, è anche possibile "nidificare" le varie promise tramite successive chiamate al metodo done, eseguendo la successive chiamata asincrona nella callback di ciascuna di queste chiamate. Il prossimo snippet chiarisce questo punto:

function loadCustomSimulator() {
    Windows.ApplicationModel.Package.current.installedLocation.getFolderAsync("data")
        .done(function (folder) {
            folder.getFileAsync("timed-trial.xml")
                .done(function (file) {
                    currentApp.licenseInformation.onlicensechanged = reloadLicense;
                    currentApp.reloadSimulatorAsync(file)
                       .done(null,
                            function (err) {
                                // gestione dell'errore
                        });
                }, function (err) {
                    // gestione dell'errore
                });
        }, function (err) {
            // gestione dell'errore
        });
}

Il primo done nella catena contiene una chiamata a un altro metodo asincrono, GetFileAsync, mentre nel secondo done il codice invoca un'altra chiamata asincrona, ReloadSimulatorAsync. Tuttavia, per evitare che un eventuale errore verificatosi durante l'esecuzione di una delle chiamate asincrone finisca per propagarsi, è necessario passare a ciascun metodo done una specifica funzione di fallback. Lo svantaggio è che questo stile tende a peggiorare rapidamente la leggibilità del codice.

Per ricapitolare quando detto sinora, quando si legano più promise tra loro, è sempre meglio chiudere la catena con una chiamata al metodo done. A differenza di then, il metodo done non nasconde eventuali eccezioni sollevate durante l'esecuzione di operazioni asincrone. Tuttavia, se non viene fornita esplicitamente una funzione di fallback o l'errore si verifica durante l'esecuzione della callback passata al metodo done, l'eccezione si propagherà e non potrà essere intercettata tramite un blocco try/catch. A questo fine, WinJS mette a disposizione uno specifico evento, WinJS.Promise.error, simile all'evento WinJS.Application.error visto in precedenza, il quale può essere usato per intercettare errori nelle promise finiti "unhandled":

WinJS.Promise.onerror = promiseErrorListener;
function promiseErrorListener(err) {
    var ex = err.detail.exception;
    var promise = err.detail.promise;
}

L'oggetto ricevuto dall'event handler consente di accedere a una serie di informazioni generali relative all'errore, come l'eccezione che è stata sollevata, la promise al cui interno l'errore si è verificato e il relative stato.

Gestire gli errori dei device

Alcune delle funzionalità (capability) dichiarate nell'application manifest, come il microfono, la camera e il location provider, sono considerati "device sensibili" ("sensitive device") poiché possono rivelare informazioni personali (come la posizione geografica) che l'utente potrebbe mantenere riservate. Per questa ragione, la prima volta che un'app cerca di accedere a uno di questi device, dichiarato nell'application manifest, il sistema chiede all'utente il permesso di utilizzare il device prima che l'app possa iniziare a usarlo. La prossima immagine mostra il dialog box con cui viene richiesto il permesso, da parte dell'app, di accedere alla webcam.

Una volta che l'utente ha prestato il suo consenso, questo può essere revocato in qualunque momento tramite le impostazioni Privacy nel pannello Permissions, attivabile mediante il charm Settings, come illustrato nella prossima immagine.

Per assicurare agli utenti una buona user experience, è importante che l'applicazione che ha bisogno di accedere a device sensibili sia in grado di gestire gli errori che possono insorgere quando si tenta di accedere a una funzionalità che non è disponibile. Le ragioni per cui l'accesso alla risorsa può fallire sono le seguenti:

  • L'utente non ha prestato il suo consenso all'uso del device nel dialog box.
  • L'utente ha revocato il permesso tramite il charm Settings.
  • Il dispositivo non è presente sul sistema.

Tuttavia, non tutte le API di WinRT presentano lo stesso comportamento quando si tenta di accedere a una capability per la quale l'app non ha i permessi necessari.

Per esempio, se l'app sfrutta la classe Windows.Media.Capture.MediaCapture per la cattura di audio, foto e video e la webcam non è presente sul sistema, una chiamata al metodo InitializeAsync dell'oggetto MediaCapture solleverà un'eccezione. La stessa cosa accade se il dispositivo è presente, ma l'utente non ha dato il permesso di accedervi (o il permesso è stato revocato). In entrambi i casi, sarà necessario gestire gli errori in modo appropriato, informando l'utente che la funzionalità richiesta non è disponibile sul sistema o che questa non può essere utilizzata fino a quando l'utente non avrà espresso il suo consenso tramite il charm Settings. Il prossimo snippet mostra come gestire un semplice scenario di questo tipo:

function startDevice_click(args) {
    mediaCapture = new Windows.Media.Capture.MediaCapture();
    mediaCapture.onrecordlimitationexceeded = recordLimitationExceeded;
    mediaCapture.onfailed = mediaFailed;
    var deviceInfo = Windows.Devices.Enumeration.DeviceInformation.findAllAsync(Windows.Devices.Enumeration.DeviceClass.videoCapture)
        .done(function (devices) {
            if (devices.length > 0) {
                var cameraSettings = new Windows.Media.Capture.MediaCaptureInitializationSettings();
                cameraSettings.videoDeviceId = devices[0].id;
                cameraSettings.streamingCaptureMode = Windows.Media.Capture.StreamingCaptureMode.audioAndVideo;
                cameraSettings.photoCaptureSource = Windows.Media.Capture.PhotoCaptureSource.videoPreview;
                cameraSettings.realTimeModeEnabled = true;
                mediaCapture.initializeAsync(cameraSettings)
                .done(null, function (err) {
                    errorMessage.innerText = "Inizializzazione fallita. Controlla i permessi per la webcam";
                });
            }
            else {
                errorMessage.innerText = "Nessuna webcam connessa";
            }
        }, function (err) {
            errorMessage.innerText = "Impossibile recuperare l'elenco dei device";
        });
}

Prima di tentare di accedere alla webcam, il codice verifica che sul sistema sia presente almeno un device per la cattura video. Quindi, se il dispositivo viene trovato, il codice tenta di inizializzarlo tramite una chiamata al metodo InitializeAsync. Se viene sollevata un'eccezione durante l'inizializzazione della webcam (in genere per la mancanza di permessi), la funzione di fallback passata come parametro al relativo metodo done avverte l'utente con un messaggio di errore.

Se invece delle MediaCapture API, l'app sfrutta la classe CaptureCameraUI, il pattern seguito è differente. Ad esempio, se manca il permesso di accedere alla webcam, la chiamata al metodo CaptureFileAsync non solleva alcuna eccezione. Al contrario, la user interface standard informerà l'utente che l'app necessita del permesso di accedere alla webcam, come mostrato nella prossima immagine.

Se invece la webcam non è presente sul sistema, il messaggio mostrato all'utente sarà diverso:

Il prossimo listato mostra un esempio di utilizzo della classe CameraCaptureUI:

function takePicture_click(args) {
    var camera = new Windows.Media.Capture.CameraCaptureUI();
    camera.videoSettings.format = Windows.Media.Capture.CameraCaptureUIPhotoFormat.jpeg;
    camera.captureFileAsync(Windows.Media.Capture.CameraCaptureUIMode.photo)
        .done(function (file) {
            if (file != null) {
                // mostra l'immagine all'utente
            } else {
                // nessuna foto catturata
            }
        }, function (err) {
            // qualcosa è andato storto
        });
}

Sebbene il metodo CameraCaptureUI.CaptureFileAsync non sollevi eccezioni se l'app non ha il permesso di accedere al device, è sempre meglio prevedere esplicitamente una funzione di fallback per gestire eventuali altri errori, perché qualcosa può sempre andare storto (ad esempio un imprevedibile malfunzionamento della webcam).


Ti consigliamo anche