Egobrain

Обработка ошибок по умолчанию AngularJS

При работе c REST Api со страницы SPA часто возникает необходимость обработки ошибок. Все бы ничего, но часть ответов об ошибках касаются запрашиваемых данных, часть – могут быть ошибками общего вида c которыми тоже надо что-то делать. Предлагаю свое решение данной проблемы.

Для решения проблемы предлагается расширить $q через делегат, добавив к нему методы success, error, defaultError. Eсли error callback в полной мере отработал ошибку, то он должен дернуть preventDefault, иначе будет вызван обработчик ошибки по умолчанию.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
app.config(['$provide', function($provide) {
    return $provide.decorator('$q', ['$delegate', function($delegate) {
        var _defer = $delegate.defer;
        $delegate.defer = function() {
            var deferred = _defer();
            var _reject = deferred.reject;
            var _resolve = deferred.resolve;
            var _then = deferred.promise.then;
            var _defaultPrevented = false;
            var _default_error_handlers = [];
            var _context = {
                preventDefault: function() {
                    _defaultPrevented = true;
                }
            };
            deferred.reject = function() {
                var args = arguments;
                _then(null, function() {
                    if (!_defaultPrevented) {
                        angular.forEach(_default_error_handlers, function(f) {
                            f.apply(_context, args);
                        });
                    }
                });
                _reject.apply(_context, arguments);
                return deferred;
            };
            deferred.promise.then = function(success, error) {
                var wrap = function(f) {
                    if (f) {
                        return function() {
                            return f.apply(_context, arguments);
                        };
                    }
                };
                return _then(success, wrap(error));
            };
            deferred.promise.success = function(fn) {
                deferred.promise.then(fn);
                return deferred.promise;
            };
            deferred.promise.error = function(fn) {
                deferred.promise.then(null, fn);
                return deferred.promise;
            };
            deferred.promise.defaultError = function(fn) {
                _default_error_handlers.push(fn);
                return deferred.promise;
            };
            return deferred;
        };
        return $delegate;
    }]);
}]);

Пример использования:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var def = $q.defer();

var promise = def.promise;
promise.defaultError(function(error) {
    alert('Error :' + error.description);
})

promise.success(function(data) {
    // Display new data ...
})
promise.error(function(error) {
    if (error.type === "wrong_data") {
        // highlite form fields
        ...
        this.preventDefault();
    }
})

def.reject({type: "unauthed"}); // Alarm will be shown
// Orelse
def.reject({type: "wrong_data"}); // Alarm won't be shown

Теперь сервис $http. Для него лучше всего сделать свою сервис-обертку, к примеру Server. Который и будет задавать обработчик ошибок по-умолчанию

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function myDefaultErrorHandler(error) {
    // ...
}

function wrap(promise) {
    var def = $q.defer();
    promise.success(function() {
        def.resolve.apply(this, arguments);
    })
    promise.error(function() {
        def.reject.apply(this, arguments);
    });
    def.promise.defaultError(myDefaultErrorHandler);
}

app.service('Server', function($http) {
    this.get = function(url, data) {
        return wrap($http({method: 'GET', url: url, params: data}));
    };
    this.post = function(url, data) {
        return wrap($http({method: 'POST', url: url, data: data}));
    };
    ...
})

Вот и все… Теперь используем Server взамен $http и обрабатываем только те ошибки, с которыми знаем что делать в данном контексте, об остальных позаботится myDefaultErrorHandler;

1
2
3
4
5
6
7
8
9
10
Server.post('/process', data)
.success(function(data) {
    // update views
})
.error(function(error) {
    if(error.type === "wrong_data") {
        // highlite for fields
        this.preventDefault();
    }
})

P.S. Можно еще много улучшать этот код: реализовать Server через провайдер, добавить к нему метод setDefaultErrorHandler и т.п…

P.P.S. В angular есть баг связанный с делегатами для $q. Дело в том, что даже если объявить новую функцию для promise в делегатe, при вызове promise.then() вернется объект без нее. Проблема в том, что в коде angular через замыкание всегда используется старая версия функции defer. Я написал PR, но судьба его пока не известна…

Комментарии