理解ASP.NET Core - 錯誤處理(Handle Errors)
注:本文隸屬於《理解ASP.NET Core》系列文章,請檢視置頂部落格或點選此處檢視全文目錄
使用中介軟體進行錯誤處理
開發人員異常頁
開發人員異常頁用於顯示未處理的請求異常的詳細資訊。當我們通過ASP.NET Core模板建立一個專案時,Startup.Configure
方法中會自動生成以下程式碼:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { // 新增開發人員異常頁中介軟體 app.UseDeveloperExceptionPage(); } }
需要注意的是,與“異常處理”有關的中介軟體,一定要儘早新增,這樣,它可以最大限度的捕獲後續中介軟體丟擲的未處理異常。
可以看到,當程式執行在開發環境中時,才會啟用開發人員異常頁,這很好理解,因為在生產環境中,我們不能將異常的詳細資訊暴露給使用者,否則,這將會導致一系列安全問題。
現在我們在下方新增如下程式碼丟擲一個異常:
app.Use((context, next) =>
{
throw new NotImplementedException();
});
當開發人員異常頁中介軟體捕獲了該未處理異常時,會展示類似如下的相關資訊:
該異常頁面展示瞭如下資訊:
- 異常訊息
- 異常堆疊追蹤(Stack)
- HTTP請求查詢引數(Query)
- Cookies
- HTTP請求標頭(Headers)
- 路由(Routing),包含了終結點和路由資訊
IDeveloperPageExceptionFilter
當你檢視DeveloperExceptionPageMiddleware
的原始碼時,你會在建構函式中發現一個入參,型別為IEnumerable<IDeveloperPageExceptionFilter>
。通過這個Filter集合,組成一個錯誤處理器管道,按照先註冊先執行的原則,順序進行錯誤處理。
下面是DeveloperExceptionPageMiddleware
的核心原始碼:
public class DeveloperExceptionPageMiddleware { public DeveloperExceptionPageMiddleware( RequestDelegate next, IOptions<DeveloperExceptionPageOptions> options, ILoggerFactory loggerFactory, IWebHostEnvironment hostingEnvironment, DiagnosticSource diagnosticSource, IEnumerable<IDeveloperPageExceptionFilter> filters) { // ... // 將 DisplayException 放置在管道最底部 // DisplayException 就用於向響應中寫入我們上面見到的異常頁 _exceptionHandler = DisplayException; foreach (var filter in filters.Reverse()) { var nextFilter = _exceptionHandler; _exceptionHandler = errorContext => filter.HandleExceptionAsync(errorContext, nextFilter); } } public async Task Invoke(HttpContext context) { try { await _next(context); } catch (Exception ex) { // 響應已經啟動,則跳過處理,直接上拋 if (context.Response.HasStarted) { throw; } try { context.Response.Clear(); context.Response.StatusCode = 500; // 錯誤處理 await _exceptionHandler(new ErrorContext(context, ex)); // ... // 錯誤已成功處理 return; } catch (Exception ex2) { } // 若處理過程中丟擲了新的異常ex2,則重新引發原始異常ex throw; } } }
這也就說明,如果我們想要自定義開發者異常頁,那我們可以通過實現IDeveloperPageExceptionFilter
介面來達到目的。
先看一下IDeveloperPageExceptionFilter
介面定義:
public interface IDeveloperPageExceptionFilter
{
Task HandleExceptionAsync(ErrorContext errorContext, Func<ErrorContext, Task> next);
}
public class ErrorContext
{
public ErrorContext(HttpContext httpContext, Exception exception)
{
HttpContext = httpContext ?? throw new ArgumentNullException(nameof(httpContext));
Exception = exception ?? throw new ArgumentNullException(nameof(exception));
}
public HttpContext HttpContext { get; }
public Exception Exception { get; }
}
HandleExceptionAsync
方法除了錯誤上下文資訊外,還包含了一個Func<ErrorContext, Task> next
,這是幹嘛的呢?其實,前面我們已經提到了,IDeveloperPageExceptionFilter
的所有實現,會組成一個管道,當錯誤需要在管道中的後續處理器作進一步處理時,就是通過這個next
傳遞錯誤的,所以,當需要傳遞錯誤時,一定要記得呼叫next
!
不廢話了,趕緊實現一個看看效果吧:
public class MyDeveloperPageExceptionFilter : IDeveloperPageExceptionFilter
{
public Task HandleExceptionAsync(ErrorContext errorContext, Func<ErrorContext, Task> next)
{
errorContext.HttpContext.Response.WriteAsync($"MyDeveloperPageExceptionFilter: {errorContext.Exception}");
// 我們不呼叫 next,這樣就不會執行 DisplayException
return Task.CompletedTask;
}
}
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IDeveloperPageExceptionFilter, MyDeveloperPageExceptionFilter>();
}
當丟擲一個異常,你會看到類似如下的頁面:
異常處理程式
上面介紹了開發環境中的異常處理,現在我們來看一下生產環境中的異常處理,通過呼叫UseExceptionHandler
擴充套件方法註冊中介軟體ExceptionHandlerMiddleware
。
該異常處理程式:
- 可以捕獲後續中介軟體未處理的異常
- 若無異常或HTTP響應已經啟動(
Response.HasStarted == true
),則不做任何處理 - 不會改變URL中的路徑
預設情況下,會生成類似如下的模板:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// 新增異常處理程式
app.UseExceptionHandler("/Home/Error");
}
}
通過lambda提供異常處理程式
我們可以通過lambda向UseExceptionHandler
中提供一個異常處理邏輯:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseExceptionHandler(errorApp =>
{
var loggerFactory = errorApp.ApplicationServices.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger("ExceptionHandlerWithLambda");
errorApp.Run(async context =>
{
// 這裡可以自定義 http response 內容,以下僅是示例
var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
logger.LogError($"Exception Handled:{exceptionHandlerPathFeature.Error}");
var statusCode = StatusCodes.Status500InternalServerError;
var message = exceptionHandlerPathFeature.Error.Message;
if (exceptionHandlerPathFeature.Error is NotImplementedException)
{
message = "俺未實現";
statusCode = StatusCodes.Status501NotImplemented;
}
context.Response.StatusCode = statusCode;
context.Response.ContentType = "application/json";
await context.Response.WriteAsJsonAsync(new
{
Message = message,
Success = false,
});
});
});
}
可以看到,當捕獲到異常時,可以通過HttpContext.Features
,並指定型別IExceptionHandlerPathFeature
或IExceptionHandlerFeature
(前者繼承自後者),來獲取到異常資訊。
public interface IExceptionHandlerFeature
{
// 異常資訊
Exception Error { get; }
}
public interface IExceptionHandlerPathFeature : IExceptionHandlerFeature
{
// 未被轉義的http請求資源路徑
string Path { get; }
}
再提醒一遍,千萬不要將敏感的錯誤資訊暴露給客戶端。
異常處理程式頁
除了使用lambda外,我們還可以指定一個路徑,指向一個備用管道進行異常處理,這個備用管道對於MVC來說,一般是Controller中的Action,例如MVC模板預設的/Home/Error
:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseExceptionHandler("/Home/Error");
}
public class HomeController : Controller
{
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
當捕獲到異常時,你會看到類似如下的頁面:
你可以在ActionError
中自定義錯誤處理邏輯,就像lambda一樣。
需要注意的是,不要隨意對Error
新增[HttpGet]
、[HttpPost]
等限定Http請求方法的特性。一旦你加上了[HttpGet]
,那麼該方法只能處理Get
請求的異常。
不過,如果你就是打算將不同方法的Http請求分別進行處理,你可以類似如下進行處理:
public class HomeController : Controller
{
// 處理Get請求的異常
[HttpGet("[controller]/error")]
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult GetError()
{
_logger.LogInformation("Get Exception Handled");
return View("Error", new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
// 處理Post請求的異常
[HttpPost("[controller]/error")]
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult PostError()
{
_logger.LogInformation("Post Exception Handled");
return View("Error", new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
另外,還需要提醒一下,如果在請求備用管道(如示例中的Error
)時也報錯了,無論是Http請求管道中的中介軟體報錯,還是Error
裡面報錯,此時ExceptionHandlerMiddleware
均會重新引發原始異常,而不是向外丟擲備用管道的異常。
一般異常處理程式頁是面向所有使用者的,所以請保證它可以匿名訪問。
下面一塊看一下ExceptionHandlerMiddleware
吧:
public class ExceptionHandlerMiddleware
{
public ExceptionHandlerMiddleware(
RequestDelegate next,
ILoggerFactory loggerFactory,
IOptions<ExceptionHandlerOptions> options,
DiagnosticListener diagnosticListener)
{
// 要麼手動指定一個異常處理器(如通過lambda)
// 要麼提供一個資源路徑,重新發送給後續中介軟體,進行異常處理
if (_options.ExceptionHandler == null)
{
if (_options.ExceptionHandlingPath == null)
{
throw new InvalidOperationException(Resources.ExceptionHandlerOptions_NotConfiguredCorrectly);
}
else
{
_options.ExceptionHandler = _next;
}
}
}
public Task Invoke(HttpContext context)
{
ExceptionDispatchInfo edi;
try
{
var task = _next(context);
if (!task.IsCompletedSuccessfully)
{
return Awaited(this, context, task);
}
return Task.CompletedTask;
}
catch (Exception exception)
{
edi = ExceptionDispatchInfo.Capture(exception);
}
// 同步完成並丟擲異常時,進行處理
return HandleException(context, edi);
static async Task Awaited(ExceptionHandlerMiddleware middleware, HttpContext context, Task task)
{
ExceptionDispatchInfo edi = null;
try
{
await task;
}
catch (Exception exception)
{
edi = ExceptionDispatchInfo.Capture(exception);
}
if (edi != null)
{
// 非同步完成並丟擲異常時,進行處理
await middleware.HandleException(context, edi);
}
}
}
private async Task HandleException(HttpContext context, ExceptionDispatchInfo edi)
{
// 響應已經啟動,則跳過處理,直接上拋
if (context.Response.HasStarted)
{
edi.Throw();
}
PathString originalPath = context.Request.Path;
if (_options.ExceptionHandlingPath.HasValue)
{
context.Request.Path = _options.ExceptionHandlingPath;
}
try
{
ClearHttpContext(context);
// 將 exceptionHandlerFeature 存入 context.Features
var exceptionHandlerFeature = new ExceptionHandlerFeature()
{
Error = edi.SourceException,
Path = originalPath.Value,
};
context.Features.Set<IExceptionHandlerFeature>(exceptionHandlerFeature);
context.Features.Set<IExceptionHandlerPathFeature>(exceptionHandlerFeature);
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
context.Response.OnStarting(_clearCacheHeadersDelegate, context.Response);
// 處理異常
await _options.ExceptionHandler(context);
if (context.Response.StatusCode != StatusCodes.Status404NotFound || _options.AllowStatusCode404Response)
{
return;
}
}
catch (Exception ex2) { }
finally
{
// 還原請求路徑,保證瀏覽器中的Url不變
context.Request.Path = originalPath;
}
// 如果異常未被處理,則重新引發原始異常
edi.Throw();
}
}
無響應正文的Http錯誤狀態碼處理
預設情況下,當ASP.NET Core遇到沒有正文的400-599Http錯誤狀態碼時,不會為其提供頁面,而是返回狀態碼和空響應正文。可是,為了良好的使用者體驗,一般我們會對常見的錯誤狀態碼(404)提供友好的頁面,如gitee404
請注意,本節所涉及到的中介軟體與上兩節所講解的錯誤異常處理中介軟體不衝突,可以同時使用。確切的說,本節並不是處理異常,只是為了提升使用者體驗。
UseStatusCodePages
我們可以通過StatusCodePagesMiddleware
中介軟體實現該功能:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseDeveloperExceptionPage();
// 新增 StatusCodePagesMiddleware 中介軟體
app.UseStatusCodePages();
// ...請求處理中介軟體
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
注意,一定要在異常處理中介軟體之後,請求處理中介軟體之前呼叫
UseStatusCodePages
。
現在,你可以請求一個不存在的路徑,例如Home/Index2
,你會在瀏覽器中看到如下輸出:
Status Code: 404; Not Found
UseStatusCodePages
也提供了過載,允許我們自定義響應內容型別和正文內容,如:
// 使用佔位符 {0} 來填充Http狀態碼
app.UseStatusCodePages("text/plain", "Status code is: {0}");
瀏覽器輸出為:
Status code is: 404
同樣地,我們也可以通過向UseStatusCodePages
傳入lambda表示式進行處理:
app.UseStatusCodePages(async context =>
{
context.HttpContext.Response.ContentType = "text/plain";
await context.HttpContext.Response.WriteAsync(
$"Status code is: {context.HttpContext.Response.StatusCode}");
});
介紹了那麼多,你也看到了,事實上UseStatusCodePages
效果並不好,所以我們在生產環境一般是不會用這玩意的,那用啥呢?請隨我繼續往下看。
UseStatusCodePagesWithRedirects
該擴充套件方法,內部實際上是通過呼叫UseStatusCodePages
並傳入lambda進行實現的,該方法:
- 接收一個Http資源定位字串。同樣的,會有一個佔位符
{0}
,用於填充Http狀態碼 - 向客戶端傳送Http狀態碼302-已找到
- 然後將客戶端重定向到指定的終結點,在該終結點中,可以針對不同錯誤狀態碼分別進行處理
app.UseStatusCodePagesWithRedirects("/Home/StatusCodeError?code={0}");
public class HomeController : Controller
{
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult StatusCodeError(int code)
{
return code switch
{
// 跳轉到404頁面
StatusCodes.Status404NotFound => View("404"),
// 跳轉到統一展示頁面
_ => View(code),
};
}
}
現在你可以自己試一下。
不知道你有沒有注意:當我們請求一個不存在的路徑時,它的確會跳轉到404頁面,但是,Url也變了,變成了/Home/StatusCodeError?code=404
,而且,響應狀態碼也變了,變成了200Ok
。可以通過原始碼看一下咋回事(我相信,大家看到302其實也都明白了):
public static IApplicationBuilder UseStatusCodePagesWithRedirects(this IApplicationBuilder app, string locationFormat)
{
// 兩個條件分支都差不多,我們看第二個,容易理解一些
if (locationFormat.StartsWith("~"))
{
locationFormat = locationFormat.Substring(1);
return app.UseStatusCodePages(context =>
{
var location = string.Format(CultureInfo.InvariantCulture, locationFormat, context.HttpContext.Response.StatusCode);
context.HttpContext.Response.Redirect(context.HttpContext.Request.PathBase + location);
return Task.CompletedTask;
});
}
else
{
return app.UseStatusCodePages(context =>
{
// 格式化資源定位,context.HttpContext.Response.StatusCode 作佔位符
var location = string.Format(CultureInfo.InvariantCulture, locationFormat, context.HttpContext.Response.StatusCode);
// 重定向(302)到設定的資源
context.HttpContext.Response.Redirect(location);
return Task.CompletedTask;
});
}
}
如果你不想更改原始請求的Url,而且保留原始狀態碼,那麼你應該使用接下來要介紹的UseStatusCodePagesWithReExecute
。
UseStatusCodePagesWithReExecute
同樣的,該擴充套件方法,內部也是通過呼叫UseStatusCodePages
並傳入lambda進行實現的,不過該方法:
- 接收1個路徑字串和和1個查詢字串。同樣的,會有一個佔位符
{0}
,用於填充Http狀態碼 - Url保持不變,並向客戶端返回原始Http狀態碼
- 執行備用管道,用於生成響應正文
// 注意,這裡要分開寫
app.UseStatusCodePagesWithReExecute("/Home/StatusCodeError", "?code={0}");
具體例子就不再列舉了,用上面的就行了。現在來看看原始碼:
public static IApplicationBuilder UseStatusCodePagesWithReExecute(
this IApplicationBuilder app,
string pathFormat,
string queryFormat = null)
{
return app.UseStatusCodePages(async context =>
{
// 請注意,此時Http響應還未啟動
// 格式化資源路徑,context.HttpContext.Response.StatusCode 作佔位符
var newPath = new PathString(
string.Format(CultureInfo.InvariantCulture, pathFormat, context.HttpContext.Response.StatusCode));
// 格式化查詢字串,context.HttpContext.Response.StatusCode 作佔位符
var formatedQueryString = queryFormat == null ? null :
string.Format(CultureInfo.InvariantCulture, queryFormat, context.HttpContext.Response.StatusCode);
var newQueryString = queryFormat == null ? QueryString.Empty : new QueryString(formatedQueryString);
var originalPath = context.HttpContext.Request.Path;
var originalQueryString = context.HttpContext.Request.QueryString;
// 將原始請求資訊儲存下來,以便後續進行還原
context.HttpContext.Features.Set<IStatusCodeReExecuteFeature>(new StatusCodeReExecuteFeature()
{
OriginalPathBase = context.HttpContext.Request.PathBase.Value,
OriginalPath = originalPath.Value,
OriginalQueryString = originalQueryString.HasValue ? originalQueryString.Value : null,
});
context.HttpContext.SetEndpoint(endpoint: null);
var routeValuesFeature = context.HttpContext.Features.Get<IRouteValuesFeature>();
routeValuesFeature?.RouteValues?.Clear();
// 構造新請求
context.HttpContext.Request.Path = newPath;
context.HttpContext.Request.QueryString = newQueryString;
try
{
// 執行備用管道,生成響應正文
await context.Next(context.HttpContext);
}
finally
{
// 還原原始請求資訊
context.HttpContext.Request.QueryString = originalQueryString;
context.HttpContext.Request.Path = originalPath;
context.HttpContext.Features.Set<IStatusCodeReExecuteFeature>(null);
}
});
}
在MVC中,你可以通過給控制器或其中的Action方法新增
[SkipStatusCodePages]
特性,可以略過StatusCodePagesMiddleware
。
使用過濾器進行錯誤處理
除了錯誤處理中介軟體外,ASP.NET Core 還提供了異常過濾器,用於錯誤處理。
異常過濾器:
- 通過實現介面
IExceptionFilter
或IAsyncExceptionFilter
來自定義異常過濾器 - 可以捕獲Controller建立時(也就是隻捕獲建構函式中丟擲的異常)、模型繫結、Action Filter和Action中丟擲的未處理異常
- 其他地方丟擲的異常不會捕獲
本節僅介紹異常過濾器,有關過濾器的詳細內容,後續文章將會介紹
先來看一下這兩個介面:
// 僅具有標記作用,標記其為 mvc 請求管道的過濾器
public interface IFilterMetadata { }
public interface IExceptionFilter : IFilterMetadata
{
// 當丟擲異常時,該方法會捕獲
void OnException(ExceptionContext context);
}
public interface IAsyncExceptionFilter : IFilterMetadata
{
// 當丟擲異常時,該方法會捕獲
Task OnExceptionAsync(ExceptionContext context);
}
OnException
和OnExceptionAsync
方法都包含一個型別為ExceptionContext
引數,很顯然,它就是與異常有關的上下文,我們的異常處理邏輯離不開它。那接著來看一下它的結構吧:
public class ExceptionContext : FilterContext
{
// 捕獲到的未處理異常
public virtual Exception Exception { get; set; }
public virtual ExceptionDispatchInfo? ExceptionDispatchInfo { get; set; }
// 指示異常是否已被處理
// true:表示異常已被處理,異常不會再向上丟擲
// false:表示異常未被處理,異常仍會繼續向上丟擲
public virtual bool ExceptionHandled { get; set; }
// 設定響應的 IActionResult
// 如果設定了結果,也表示異常已被處理,異常不會再向上丟擲
public virtual IActionResult? Result { get; set; }
}
除此之外,ExceptionContext
還繼承了FilterContext
,而FilterContext
又繼承了ActionContext
(這也從側面說明,過濾器是為Action服務的),也就是說我們也能夠獲取到一些過濾器和Action相關的資訊,看看都有什麼吧:
public class ActionContext
{
// Action相關的資訊
public ActionDescriptor ActionDescriptor { get; set; }
// HTTP上下文
public HttpContext HttpContext { get; set; }
// 模型繫結和驗證
public ModelStateDictionary ModelState { get; }
// 路由資料
public RouteData RouteData { get; set; }
}
public abstract class FilterContext : ActionContext
{
public virtual IList<IFilterMetadata> Filters { get; }
public bool IsEffectivePolicy<TMetadata>(TMetadata policy) where TMetadata : IFilterMetadata {}
public TMetadata FindEffectivePolicy<TMetadata>() where TMetadata : IFilterMetadata {}
}
更多引數細節,我會在專門講過濾器的文章中詳細介紹。
下面,我們就來實現一個自定義的異常處理器:
public class MyExceptionFilterAttribute : ExceptionFilterAttribute
{
private readonly IModelMetadataProvider _modelMetadataProvider;
public MyExceptionFilterAttribute(IModelMetadataProvider modelMetadataProvider)
{
_modelMetadataProvider = modelMetadataProvider;
}
public override void OnException(ExceptionContext context)
{
if (!context.ExceptionHandled)
{
// 此處僅為簡單演示
var exception = context.Exception;
var result = new ViewResult()
{
ViewName = "Error",
ViewData = new ViewDataDictionary(_modelMetadataProvider, context.ModelState)
{
// 記得給 ErrorViewModel 加上 Message 屬性
Model = new ErrorViewModel
{
Message = exception.ToString()
}
}
};
context.Result = result;
// 標記異常已處理
context.ExceptionHandled = true;
}
}
}
接著,找到/Views/Shared/Error.cshtml
,展示一下錯誤訊息:
@model ErrorViewModel
@{
ViewData["Title"] = "Error";
}
<p>@Model.Message</p>
最後,將服務MyExceptionFilterAttribute
註冊到DI容器:
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<MyExceptionFilterAttribute>();
services.AddControllersWithViews();
}
現在,我們將該異常處理器加在/Home/Index
上,並拋個異常:
public class HomeController : Controller
{
[ServiceFilter(typeof(MyExceptionFilterAttribute))]
public IActionResult Index()
{
throw new Exception("Home Index Error");
return View();
}
}
當請求/Home/Index
時,你會得到如下頁面:
錯誤處理中介軟體 VS 異常過濾器
現在,我們已經介紹了兩種錯誤處理的方法——錯誤處理中介軟體和異常過濾器。現在來比較一下它們的異同,以及我們何時應該選擇哪種處理方式。
錯誤處理中介軟體:
- 可以捕獲後續中介軟體的所有未處理異常
- 擁有
RequestDelegate
,操作更加靈活 - 粒度較粗,僅可針對全域性進行配置
錯誤處理中介軟體適合用於處理全域性異常。
異常過濾器:
- 僅可捕獲Controller建立時(也就是建構函式中丟擲的異常)、模型繫結、Action Filter和Action中丟擲的未處理異常,其他地方丟擲的異常捕獲不到
- 粒度更小,可以靈活針對Controller或Action配置不同的異常過濾器
異常過濾器非常適合用於捕獲並處理Action中的異常。
在我們的應用中,可以同時使用錯誤處理中介軟體和異常過濾器,只有充分發揮它們各自的優勢,才能處理好程式中的錯誤。